diff --git a/lib/controller.js b/lib/controller.js index 15877b284..3c6ab4106 100644 --- a/lib/controller.js +++ b/lib/controller.js @@ -154,7 +154,7 @@ class Controller { await this.mqtt.connect(); // Send all cached states. - if (settings.get().advanced.cache_state) { + if (settings.get().advanced.cache_state_send_on_startup && settings.get().advanced.cache_state) { for (const device of this.zigbee.getClients()) { if (this.state.exists(device.ieeeAddr)) { this.publishEntityState(device.ieeeAddr, this.state.get(device.ieeeAddr)); diff --git a/lib/state.js b/lib/state.js index 7b429800c..fef5dda70 100644 --- a/lib/state.js +++ b/lib/state.js @@ -1,5 +1,6 @@ const logger = require('./util/logger'); const data = require('./util/data'); +const settings = require('./util/settings'); const fs = require('fs'); const objectAssignDeep = require('object-assign-deep'); @@ -53,9 +54,13 @@ class State { } save() { - logger.debug(`Saving state to file ${this.file}`); - const json = JSON.stringify(this.state, null, 4); - fs.writeFileSync(this.file, json, 'utf8'); + if (settings.get().advanced.cache_state_persistent) { + logger.debug(`Saving state to file ${this.file}`); + const json = JSON.stringify(this.state, null, 4); + fs.writeFileSync(this.file, json, 'utf8'); + } else { + logger.debug(`Not saving state`); + } } exists(ID) { diff --git a/lib/util/settings.js b/lib/util/settings.js index 4139e4d68..cf197a589 100644 --- a/lib/util/settings.js +++ b/lib/util/settings.js @@ -71,6 +71,8 @@ const defaults = { * https://www.zigbee2mqtt.io/configuration/configuration.html */ cache_state: true, + cache_state_persistent: true, + cache_state_send_on_startup: true, /** * Add a last_seen attribute to mqtt messages, contains date/time of zigbee message arrival @@ -171,6 +173,8 @@ const schema = { ext_pan_id: {type: 'array', items: {type: 'number'}}, channel: {type: 'number', minimum: 11, maximum: 26}, cache_state: {type: 'boolean'}, + cache_state_persistent: {type: 'boolean'}, + cache_state_send_on_startup: {type: 'boolean'}, log_rotation: {type: 'boolean'}, log_level: {type: 'string', enum: ['info', 'warn', 'error', 'debug']}, log_output: {type: 'array', items: {type: 'string'}}, diff --git a/test/controller.test.js b/test/controller.test.js index d55b7da64..9e030c86b 100644 --- a/test/controller.test.js +++ b/test/controller.test.js @@ -108,6 +108,15 @@ describe('Controller', () => { expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/remote", `{"brightness":255}`, {"qos": 0, "retain": true}, expect.any(Function)); }); + it('Start controller should not publish cached states when disabled', async () => { + settings.set(['advanced', 'cache_state_send_on_startup'], false); + data.writeDefaultState(); + await controller.start(); + await flushPromises(); + const publishedTopics = MQTT.publish.mock.calls.map(m => m[0]); + expect(publishedTopics).toEqual(expect.not.arrayContaining(["zigbee2mqtt/bulb", "zigbee2mqtt/remote"])); + }); + it('Start controller should not publish cached states when cache_state is false', async () => { settings.set(['advanced', 'cache_state'], false); data.writeDefaultState(); @@ -517,6 +526,21 @@ describe('Controller', () => { expect(MQTT.publish).toHaveBeenCalledTimes(0); }); + it('Should allow to disable state persistency', async () => { + settings.set(['advanced', 'cache_state_persistent'], false); + data.removeState(); + await controller.start(); + MQTT.publish.mockClear(); + await controller.publishEntityState('bulb', {state: 'ON'}); + await controller.publishEntityState('bulb', {brightness: 200}); + await flushPromises(); + expect(MQTT.publish).toHaveBeenCalledTimes(2); + expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb", JSON.stringify({state: "ON"}), {"qos": 0, "retain": true}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb", JSON.stringify({state: "ON", brightness: 200}), {"qos": 0, "retain": true}, expect.any(Function)); + await controller.stop(); + expect(data.stateExists()).toBeFalsy(); + }); + it('Publish should not cache when set', async () => { settings.set(['advanced', 'cache_state'], false); data.writeEmptyState(); diff --git a/test/stub/data.js b/test/stub/data.js index 0f1b7036f..f7177beba 100644 --- a/test/stub/data.js +++ b/test/stub/data.js @@ -5,6 +5,7 @@ const fs = require('fs'); const mockDir = tmp.dirSync().name; const mockDirStorage = tmp.dirSync().name; +const stateFile = path.join(mockDir, 'state.json'); function writeDefaultConfiguration() { const config = { @@ -182,7 +183,17 @@ function writeDefaultConfiguration() { } function writeEmptyState() { - fs.writeFileSync(path.join(mockDir, 'state.json'), JSON.stringify({})); + fs.writeFileSync(stateFile, JSON.stringify({})); +} + +function removeState() { + if (stateExists()) { + fs.unlinkSync(stateFile); + } +} + +function stateExists() { + return fs.existsSync(stateFile); } function writeDefaultState() { @@ -201,10 +212,6 @@ function writeDefaultState() { fs.writeFileSync(path.join(mockDir, 'state.json'), JSON.stringify(state)); } -function removeState() { - fs.unlinkSync(path.join(mockDir, 'state.json')) -} - jest.mock('../../lib/util/data', () => ({ joinPath: (file) => require('path').join(mockDir, file), getPath: () => mockDir, @@ -220,4 +227,5 @@ module.exports = { writeDefaultState, removeState, writeEmptyState, + stateExists, };