mirror of
https://github.com/Koenkk/zigbee2mqtt.git
synced 2026-10-23 14:22:38 +00:00
fix: Improve settings changing (#22674)
* Add namespaced levels for logger. Add NS to mqtt. Deprecate 'warn'. * Improve setting validation. * Fix setting through frontend * Support reload + frontend improvements * update description * remove requiresRestart * Fix tests. * Fix namespaced logging at lower levels. Add better tests. * fix: Fix settings changing * remove `removeNullPropertiesFromObject` from bridge * Add extra test * update --------- Co-authored-by: Nerivec <62446222+Nerivec@users.noreply.github.com>
This commit is contained in:
1 parent
2eec6a4d26
commit
ce6043acb0
7 files changed
+99
-130
No files matched your search
+7
-10
@@ -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();
|
||||
|
||||
@@ -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": [{
|
||||
|
||||
@@ -480,20 +480,22 @@ export function set(path: string[], value: string | number | boolean | KeyValue)
|
||||
write();
|
||||
}
|
||||
|
||||
export function apply(newSettings: Record<string, unknown>): boolean {
|
||||
export function apply(settings: Record<string, unknown>): 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;
|
||||
|
||||
+1
-40
@@ -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,
|
||||
};
|
||||
Generated
-6
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
+9
-2
@@ -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"}),
|
||||
|
||||
Reference in new issue
Block a user