diff --git a/lib/extension/bridge.ts b/lib/extension/bridge.ts index 4615b845f..d256a69cc 100644 --- a/lib/extension/bridge.ts +++ b/lib/extension/bridge.ts @@ -186,29 +186,29 @@ export default class Bridge extends Extension { throw new Error(`Invalid payload`); } - const newSettings = utils.computeSettingsToChange(settings.get(), message.options); + const newSettings = message.options; const restartRequired = settings.apply(newSettings); if (restartRequired) this.restartRequired = true; // Apply some settings on-the-fly. if (newSettings.permit_join != undefined) { - await this.zigbee.permitJoin(newSettings.permit_join); + await this.zigbee.permitJoin(settings.get().permit_join); } if (newSettings.homeassistant != undefined) { - await this.enableDisableExtension(newSettings.homeassistant, 'HomeAssistant'); + await this.enableDisableExtension(!!settings.get().homeassistant, 'HomeAssistant'); } if (newSettings.advanced?.log_level != undefined) { - logger.setLevel(newSettings.advanced.log_level); + logger.setLevel(settings.get().advanced.log_level); } if (newSettings.advanced?.log_namespaced_levels != undefined) { - logger.setNamespacedLevels(newSettings.advanced.log_namespaced_levels); + logger.setNamespacedLevels(settings.get().advanced.log_namespaced_levels); } if (newSettings.advanced?.log_debug_namespace_ignore != undefined) { - logger.setDebugNamespaceIgnore(newSettings.advanced.log_debug_namespace_ignore); + logger.setDebugNamespaceIgnore(settings.get().advanced.log_debug_namespace_ignore); } logger.info('Successfully changed options'); @@ -444,11 +444,8 @@ export default class Bridge extends Extension { const ID = message.id; const entity = this.getEntity(entityType, ID); - const currentOptions = entityType === 'device' ? settings.get().devices[entity.ID] : - settings.get().groups[entity.ID]; - const options = utils.computeSettingsToChange(currentOptions, message.options); const oldOptions = objectAssignDeep({}, cleanup(entity.options)); - const restartRequired = settings.changeEntityOptions(ID, options); + const restartRequired = settings.changeEntityOptions(ID, message.options); if (restartRequired) this.restartRequired = true; const newOptions = cleanup(entity.options); await this.publishInfo(); diff --git a/lib/util/settings.schema.json b/lib/util/settings.schema.json index 6559537ab..2af3ceac7 100644 --- a/lib/util/settings.schema.json +++ b/lib/util/settings.schema.json @@ -532,73 +532,82 @@ ] }, "log_syslog": { - "type": "object", - "title": "syslog", - "properties": { - "host": { - "type": "string", - "title": "Host", - "description": "The host running syslogd, defaults to localhost.", - "default": "localhost" + "requiresRestart": true, + "oneOf": [ + { + "title": "syslog (disabled)", + "type": "null" }, - "port": { - "type": "number", - "title": "Port", - "description": "The port on the host that syslog is running on, defaults to syslogd's default port.", - "default": 514 - }, - "protocol": { - "type": "string", - "title": "Protocol", - "description": "The network protocol to log over (e.g. tcp4, udp4, tls4, unix, unix-connect, etc).", - "default": "udp4", - "examples": [ - "udp4", - "tls4", - "unix", - "unix-connect" - ] - }, - "path": { - "type": "string", - "title": "Path", - "description": "The path to the syslog dgram socket (i.e. /dev/log or /var/run/syslog for OS X).", - "default": "/dev/log", - "examples": [ - "/var/run/syslog" - ] - }, - "pid": { - "type": "string", - "title": "PID", - "description": "PID of the process that log messages are coming from (Default process.pid).", - "default": "process.pid" - }, - "localhost": { - "type": "string", - "title": "Localhost", - "description": "Host to indicate that log messages are coming from (Default: localhost).", - "default": "localhost" - }, - "type": { - "type": "string", - "title": "Type", - "description": "The type of the syslog protocol to use (Default: BSD, also valid: 5424).", - "default": "5424" - }, - "app_name": { - "type": "string", - "title": "Localhost", - "description": "The name of the application (Default: Zigbee2MQTT).", - "default": "Zigbee2MQTT" - }, - "eol": { - "type": "string", - "title": "eol", - "description": "The end of line character to be added to the end of the message (Default: Message without modifications).", - "default": "/n" + { + "title": "syslog (enabled)", + "type": "object", + "properties": { + "host": { + "type": "string", + "title": "Host", + "description": "The host running syslogd, defaults to localhost.", + "default": "localhost" + }, + "port": { + "type": "number", + "title": "Port", + "description": "The port on the host that syslog is running on, defaults to syslogd's default port.", + "default": 514 + }, + "protocol": { + "type": "string", + "title": "Protocol", + "description": "The network protocol to log over (e.g. tcp4, udp4, tls4, unix, unix-connect, etc).", + "default": "udp4", + "examples": [ + "udp4", + "tls4", + "unix", + "unix-connect" + ] + }, + "path": { + "type": "string", + "title": "Path", + "description": "The path to the syslog dgram socket (i.e. /dev/log or /var/run/syslog for OS X).", + "default": "/dev/log", + "examples": [ + "/var/run/syslog" + ] + }, + "pid": { + "type": "string", + "title": "PID", + "description": "PID of the process that log messages are coming from (Default process.pid).", + "default": "process.pid" + }, + "localhost": { + "type": "string", + "title": "Localhost", + "description": "Host to indicate that log messages are coming from (Default: localhost).", + "default": "localhost" + }, + "type": { + "type": "string", + "title": "Type", + "description": "The type of the syslog protocol to use (Default: BSD, also valid: 5424).", + "default": "5424" + }, + "app_name": { + "type": "string", + "title": "Localhost", + "description": "The name of the application (Default: Zigbee2MQTT).", + "default": "Zigbee2MQTT" + }, + "eol": { + "type": "string", + "title": "eol", + "description": "The end of line character to be added to the end of the message (Default: Message without modifications).", + "default": "/n" + } + } } - } + ] }, "pan_id": { "oneOf": [{ diff --git a/lib/util/settings.ts b/lib/util/settings.ts index 825f55557..342f00175 100644 --- a/lib/util/settings.ts +++ b/lib/util/settings.ts @@ -480,20 +480,22 @@ export function set(path: string[], value: string | number | boolean | KeyValue) write(); } -export function apply(newSettings: Record): boolean { +export function apply(settings: Record): boolean { + getInternalSettings(); // Ensure _settings is initialized. + /* eslint-disable-line */ // @ts-ignore + const newSettings = objectAssignDeep.noMutate(_settings, settings); + utils.removeNullPropertiesFromObject(newSettings); ajvSetting(newSettings); const errors = ajvSetting.errors && ajvSetting.errors.filter((e) => e.keyword !== 'required'); - if (errors.length) { + if (errors?.length) { const error = errors[0]; throw new Error(`${error.instancePath.substring(1)} ${error.message}`); } - getInternalSettings(); // Ensure _settings is initialized. - /* eslint-disable-line */ // @ts-ignore - _settings = objectAssignDeep.noMutate(_settings, newSettings); + _settings = newSettings; write(); - ajvRestartRequired(newSettings); + ajvRestartRequired(settings); const restartRequired = ajvRestartRequired.errors && !!ajvRestartRequired.errors.find((e) => e.keyword === 'requiresRestart'); return restartRequired; diff --git a/lib/util/utils.ts b/lib/util/utils.ts index 78b53e3f0..faec54aff 100644 --- a/lib/util/utils.ts +++ b/lib/util/utils.ts @@ -4,8 +4,6 @@ import data from './data'; import vm from 'vm'; import fs from 'fs'; import path from 'path'; -import {detailedDiff} from 'deep-object-diff'; -import objectAssignDeep from 'object-assign-deep'; import type * as zhc from 'zigbee-herdsman-converters'; // construct a local ISO8601 string (instead of UTC-based) @@ -336,10 +334,6 @@ function filterProperties(filter: string[], data: KeyValue): void { } } -function clone(obj: KeyValue): KeyValue { - return JSON.parse(JSON.stringify(obj)); -} - export function isNumericExposeFeature(feature: zhc.Feature): feature is zhc.Numeric { return feature?.type === 'numeric'; } @@ -352,39 +346,6 @@ export function isBinaryExposeFeature(feature: zhc.Feature): feature is zhc.Bina return feature?.type === 'binary'; } -function computeSettingsToChange(current: KeyValue, new_: KeyValue): KeyValue { - const diff: KeyValue = detailedDiff(current, new_); - - // Remove any settings that are in the deleted.diff but not in the passed options - const cleanupDeleted = (options: KeyValue, deleted: KeyValue): void => { - for (const key of Object.keys(deleted)) { - if (!(key in options)) { - delete deleted[key]; - } else if (!Array.isArray(options[key])) { - cleanupDeleted(options[key], deleted[key]); - } - } - }; - cleanupDeleted(new_, diff.deleted); - - // objectAssignDeep requires object prototype which is missing from detailedDiff, therefore clone - const newSettings = objectAssignDeep({}, clone(diff.added), clone(diff.updated), clone(diff.deleted)); - - // deep-object-diff converts arrays to objects, set original array back here - const convertBackArray = (before: KeyValue, after: KeyValue): void => { - for (const [key, afterValue] of Object.entries(after)) { - const beforeValue = before[key]; - if (Array.isArray(beforeValue)) { - after[key] = beforeValue; - } else if (afterValue && typeof beforeValue === 'object') { - convertBackArray(beforeValue, afterValue); - } - } - }; - convertBackArray(new_, newSettings); - return newSettings; -} - function getScenes(entity: zh.Endpoint | zh.Group): Scene[] { const scenes: {[id: number]: Scene} = {}; const endpoints = isEndpoint(entity) ? [entity] : entity.members; @@ -410,5 +371,5 @@ export default { removeNullPropertiesFromObject, toNetworkAddressHex, toSnakeCase, isEndpoint, isZHGroup, hours, minutes, seconds, validateFriendlyName, sleep, sanitizeImageParameter, isAvailabilityEnabledForEntity, publishLastSeen, availabilityPayload, - getAllFiles, filterProperties, flatten, arrayUnique, clone, computeSettingsToChange, getScenes, + getAllFiles, filterProperties, flatten, arrayUnique, getScenes, }; diff --git a/package-lock.json b/package-lock.json index bc66fc7a0..e5abf928c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,6 @@ "bind-decorator": "^1.0.11", "connect-gzip-static": "3.0.1", "debounce": "^2.0.0", - "deep-object-diff": "^1.1.9", "fast-deep-equal": "^3.1.3", "finalhandler": "^1.2.0", "git-last-commit": "^1.0.1", @@ -4434,11 +4433,6 @@ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, - "node_modules/deep-object-diff": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/deep-object-diff/-/deep-object-diff-1.1.9.tgz", - "integrity": "sha512-Rn+RuwkmkDwCi2/oXOFS9Gsr5lJZu/yTGpK7wAaAIE75CC+LCGEZHpY6VQJa/RoJcrmaA/docWJZvYohlNkWPA==" - }, "node_modules/deepmerge": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", diff --git a/package.json b/package.json index 9a2506de9..e60b399c3 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,6 @@ "bind-decorator": "^1.0.11", "connect-gzip-static": "3.0.1", "debounce": "^2.0.0", - "deep-object-diff": "^1.1.9", "fast-deep-equal": "^3.1.3", "finalhandler": "^1.2.0", "git-last-commit": "^1.0.1", diff --git a/test/bridge.test.js b/test/bridge.test.js index 09f53efa6..3eff931dc 100644 --- a/test/bridge.test.js +++ b/test/bridge.test.js @@ -1309,9 +1309,11 @@ describe('Bridge', () => { it('Change options and apply - log_namespaced_levels', async () => { logger.setLevel('info'); + settings.apply({advanced: {log_namespaced_levels: {'zh:zstack': 'warning', 'z2m:mqtt': 'debug'}}}); MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/options', stringify({options: {advanced: {log_namespaced_levels: { "z2m:mqtt": 'warning' }}}})); + MQTT.events.message('zigbee2mqtt/bridge/request/options', stringify({options: {advanced: {log_namespaced_levels: { "z2m:mqtt": 'warning', "zh:zstack": null }}}})); await flushPromises(); + expect(settings.get().advanced.log_namespaced_levels).toStrictEqual({"z2m:mqtt": "warning"}); expect(logger.getNamespacedLevels()).toStrictEqual({ "z2m:mqtt": 'warning' }); expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/info', expect.any(String), { retain: true, qos: 0 }, expect.any(Function)); expect(MQTT.publish).toHaveBeenCalledWith( @@ -1319,6 +1321,11 @@ describe('Bridge', () => { stringify({"data":{"restart_required":false},"status":"ok"}), {retain: false, qos: 0}, expect.any(Function) ); + + MQTT.events.message('zigbee2mqtt/bridge/request/options', stringify({options: {advanced: {log_namespaced_levels: { "z2m:mqtt": null }}}})); + await flushPromises(); + expect(settings.get().advanced.log_namespaced_levels).toStrictEqual({}); + expect(logger.getNamespacedLevels()).toStrictEqual({}); }); it('Change options restart required', async () => { @@ -1355,7 +1362,7 @@ describe('Bridge', () => { MQTT.publish.mockClear(); MQTT.events.message('zigbee2mqtt/bridge/request/options', stringify({"options":{"serial":{"disable_led":false,"port":null}}})); await flushPromises(); - expect(settings.get().serial).toStrictEqual({"disable_led": false, "port": null}); + expect(settings.get().serial).toStrictEqual({"disable_led": false}); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/options', stringify({"data":{"restart_required":true},"status":"ok"}),