diff --git a/lib/util/data.js b/lib/util/data.js index 0ce7eca84..7b5f9f94f 100644 --- a/lib/util/data.js +++ b/lib/util/data.js @@ -2,14 +2,21 @@ const path = require('path'); let dataPath = null; -if (process.env.ZIGBEE2MQTT_DATA) { - dataPath = process.env.ZIGBEE2MQTT_DATA; -} else { - dataPath = path.join(__dirname, '..', '..', 'data'); - dataPath = path.normalize(dataPath); +function load() { + if (process.env.ZIGBEE2MQTT_DATA) { + dataPath = process.env.ZIGBEE2MQTT_DATA; + } else { + dataPath = path.join(__dirname, '..', '..', 'data'); + dataPath = path.normalize(dataPath); + } } +load(); + module.exports = { joinPath: (file) => path.join(dataPath, file), getPath: () => dataPath, + + // For test only. + _reload: () => load(), }; diff --git a/lib/util/settings.js b/lib/util/settings.js index 37ca50931..6976d2726 100644 --- a/lib/util/settings.js +++ b/lib/util/settings.js @@ -71,11 +71,46 @@ function writeRead() { } function write() { - fs.writeFileSync(file, yaml.safeDump(settings)); + const toWrite = objectAssignDeep.noMutate(settings); + + // Read settings to check if we have to split devices/groups into seperate file. + const actual = readYaml(file); + if (typeof actual.devices === 'string') { + writeYaml(data.joinPath(actual.devices), settings.devices); + toWrite.devices = actual.devices; + } + + if (typeof actual.groups === 'string') { + writeYaml(data.joinPath(actual.groups), settings.groups); + toWrite.groups = actual.groups; + } + + writeYaml(file, toWrite); +} + +function readYaml(file) { + return yaml.safeLoad(fs.readFileSync(file, 'utf8')); +} + +function writeYaml(file, content) { + fs.writeFileSync(file, yaml.safeDump(content)); } function read() { - return yaml.safeLoad(fs.readFileSync(file, 'utf8')); + const s = readYaml(file); + + // Read devices/groups configuration from separate file. + if (typeof s.devices === 'string') { + const file = data.joinPath(s.devices); + s.devices = fs.existsSync(file) ? readYaml(file) : null; + } + + if (typeof s.groups === 'string') { + const file = data.joinPath(s.groups); + s.groups = fs.existsSync(file) ? readYaml(file) : null; + } + + return s; } function set(path, value) { @@ -178,4 +213,9 @@ module.exports = { changeDeviceOptions: (ieeeAddr, options) => changeDeviceOptions(ieeeAddr, options), addOnChangeHandler: (handler) => onChangeHandlers.push(handler), + + // For test + _getDefaults: () => { + return objectAssignDeep.noMutate(defaults); + }, }; diff --git a/package.json b/package.json index e5b66bc5f..3692b04e5 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "mocha": "*", "nyc": "*", "proxyquire": "*", - "sinon": "*" + "sinon": "*", + "tmp": "*" } } diff --git a/test/data.test.js b/test/data.test.js index 1b414ff48..c37a2375a 100644 --- a/test/data.test.js +++ b/test/data.test.js @@ -1,21 +1,23 @@ const chai = require('chai'); -const proxyquire = require('proxyquire').noPreserveCache(); -const data = () => proxyquire('../lib/util/data.js', {}); +const data = require('../lib/util/data.js'); const path = require('path'); describe('Data', () => { describe('Get path', () => { it('Should return correct path', () => { const expected = path.normalize(path.join(__dirname, '..', 'data')); - const actual = data().getPath(); + const actual = data.getPath(); chai.assert.strictEqual(actual, expected); }); it('Should return correct path when ZIGBEE2MQTT_DATA set', () => { const expected = path.join('var', 'zigbee2mqtt'); process.env.ZIGBEE2MQTT_DATA = expected; - const actual = data().getPath(); + data._reload(); + const actual = data.getPath(); chai.assert.strictEqual(actual, expected); + delete process.env.ZIGBEE2MQTT_DATA; + data._reload(); }); }); }); diff --git a/test/settings.test.js b/test/settings.test.js new file mode 100644 index 000000000..b427abb29 --- /dev/null +++ b/test/settings.test.js @@ -0,0 +1,290 @@ +const chai = require('chai'); +const sinon = require('sinon'); +const data = require('../lib/util/data'); +const proxyquire = require('proxyquire').noPreserveCache(); +const settingsProxy = () => proxyquire('../lib/util/settings.js', {}); +const tmp = require('tmp'); +const sandbox = sinon.createSandbox(); +const path = require('path'); +const fs = require('fs'); +const yaml = require('js-yaml'); +const rimraf = require('rimraf'); + +describe('Settings', () => { + let dir = null; + let settings = null; + let configurationFile = null; + + const write = (file, json) => { + fs.writeFileSync(file, yaml.safeDump(json)); + }; + + const read = (file) => { + return yaml.safeLoad(fs.readFileSync(file, 'utf8')); + }; + + const setup = (configuration) => { + configurationFile = path.join(dir.name, 'configuration.yaml'); + write(configurationFile, configuration); + settings = settingsProxy(); + }; + + beforeEach(() => { + dir = tmp.dirSync(); + sandbox.stub(data, 'joinPath').callsFake((file) => { + return path.join(dir.name, file); + }); + }); + + afterEach(() => { + rimraf.sync(dir.name); + sandbox.restore(); + }); + + describe('Settings', () => { + it('Should return default settings', () => { + setup({}); + const s = settings.get(); + chai.assert.deepEqual(s, settings._getDefaults()); + }); + + it('Should return settings', () => { + setup({permit_join: true}); + const s = settings.get(); + const expected = settings._getDefaults(); + expected.permit_join = true; + chai.assert.deepEqual(s, expected); + }); + + it('Should add devices', () => { + setup({}); + settings.addDevice('0x12345678'); + + const actual = read(configurationFile); + const expected = { + devices: { + '0x12345678': { + friendly_name: '0x12345678', + retain: false, + }, + }, + }; + + chai.assert.deepEqual(actual, expected); + }); + + it('Should read devices', () => { + const content = { + devices: { + '0x12345678': { + friendly_name: '0x12345678', + retain: false, + }, + }, + }; + + setup(content); + + const device = settings.getDevice('0x12345678'); + const expected = { + friendly_name: '0x12345678', + retain: false, + }; + + chai.assert.deepEqual(device, expected); + }); + + it('Should read devices form a seperate file', () => { + const contentConfiguration = { + devices: 'devices.yaml', + }; + + const contentDevices = { + '0x12345678': { + friendly_name: '0x12345678', + retain: false, + }, + }; + + write(path.join(dir.name, 'devices.yaml'), contentDevices); + setup(contentConfiguration); + + const device = settings.getDevice('0x12345678'); + const expected = { + friendly_name: '0x12345678', + retain: false, + }; + + chai.assert.deepEqual(device, expected); + }); + + it('Should add devices to a seperate file', () => { + const contentConfiguration = { + devices: 'devices.yaml', + }; + + const contentDevices = { + '0x12345678': { + friendly_name: '0x12345678', + retain: false, + }, + }; + + const devicesFile = path.join(dir.name, 'devices.yaml'); + write(devicesFile, contentDevices); + setup(contentConfiguration); + + settings.addDevice('0x1234'); + + chai.assert.deepEqual(read(configurationFile), {devices: 'devices.yaml'}); + + const expected = { + '0x12345678': { + friendly_name: '0x12345678', + retain: false, + }, + '0x1234': { + friendly_name: '0x1234', + retain: false, + }, + }; + + chai.assert.deepEqual(read(devicesFile), expected); + }); + + it('Should add devices to a seperate file if devices.yaml doesnt exist', () => { + const contentConfiguration = { + devices: 'devices.yaml', + }; + + const devicesFile = path.join(dir.name, 'devices.yaml'); + setup(contentConfiguration); + + settings.addDevice('0x1234'); + + chai.assert.deepEqual(read(configurationFile), {devices: 'devices.yaml'}); + + const expected = { + '0x1234': { + friendly_name: '0x1234', + retain: false, + }, + }; + + chai.assert.deepEqual(read(devicesFile), expected); + }); + + it('Should add and remove devices to a seperate file if devices.yaml doesnt exist', () => { + const contentConfiguration = { + devices: 'devices.yaml', + }; + + const devicesFile = path.join(dir.name, 'devices.yaml'); + setup(contentConfiguration); + + settings.addDevice('0x1234'); + chai.assert.deepEqual(read(configurationFile), {devices: 'devices.yaml'}); + + settings.removeDevice('0x1234'); + chai.assert.deepEqual(read(configurationFile), {devices: 'devices.yaml'}); + + chai.assert.deepEqual(read(devicesFile), {}); + }); + + it('Should read groups', () => { + const content = { + groups: { + '1': { + friendly_name: '123', + }, + }, + }; + + setup(content); + + const group = settings.getGroup('1'); + const expected = { + friendly_name: '123', + }; + + chai.assert.deepEqual(group, expected); + }); + + it('Should read groups form a seperate file', () => { + const contentConfiguration = { + groups: 'groups.yaml', + }; + + const contentGroups = { + '1': { + friendly_name: '123', + }, + }; + + write(path.join(dir.name, 'groups.yaml'), contentGroups); + setup(contentConfiguration); + + const group = settings.getGroup('1'); + const expected = { + friendly_name: '123', + }; + + chai.assert.deepEqual(group, expected); + }); + + it('Combine everything! groups and devices from separte file :)', () => { + const contentConfiguration = { + devices: 'devices.yaml', + groups: 'groups.yaml', + }; + + const contentGroups = { + '1': { + friendly_name: '123', + }, + }; + + write(path.join(dir.name, 'groups.yaml'), contentGroups); + setup(contentConfiguration); + + const devicesFile = path.join(dir.name, 'devices.yaml'); + setup(contentConfiguration); + + const expectedConfiguration = { + devices: 'devices.yaml', + groups: 'groups.yaml', + }; + + chai.assert.deepEqual(read(configurationFile), expectedConfiguration); + + settings.addDevice('0x1234'); + + chai.assert.deepEqual(read(configurationFile), expectedConfiguration); + + const expectedDevice = { + '0x1234': { + friendly_name: '0x1234', + retain: false, + }, + }; + + chai.assert.deepEqual(read(devicesFile), expectedDevice); + + const group = settings.getGroup('1'); + const expectedGroup = { + friendly_name: '123', + }; + + chai.assert.deepEqual(group, expectedGroup); + + chai.assert.deepEqual(read(configurationFile), expectedConfiguration); + + const expectedDevice2 = { + friendly_name: '0x1234', + retain: false, + }; + + chai.assert.deepEqual(settings.getDevice('0x1234'), expectedDevice2); + }); + }); +});