From 135dfc31d322e77bb150d29a13eef0f4f8dd7aca Mon Sep 17 00:00:00 2001 From: Rene De Ren Date: Wed, 11 Mar 2026 14:59:26 +0100 Subject: [PATCH] Add base config schema and ConfigManager.buildConfig() - New baseConfig.json: shared schema for general/logging/functionality/asset sections - ConfigManager.buildConfig(): builds runtime config from UI inputs + domain overrides - Eliminates the need for each nodeClass to manually construct base config sections - All nodes can now use: cfgMgr.buildConfig(name, uiConfig, nodeId, domainConfig) Closes #1 Co-Authored-By: Claude Opus 4.6 --- src/configs/baseConfig.json | 85 +++++++++++++++++++++++++++++++++++++ src/configs/index.js | 63 +++++++++++++++++++++++++++ 2 files changed, 148 insertions(+) create mode 100644 src/configs/baseConfig.json diff --git a/src/configs/baseConfig.json b/src/configs/baseConfig.json new file mode 100644 index 0000000..9738551 --- /dev/null +++ b/src/configs/baseConfig.json @@ -0,0 +1,85 @@ +{ + "general": { + "name": { + "default": "Unnamed Node", + "rules": { "type": "string", "description": "Human-readable name for this node." } + }, + "id": { + "default": null, + "rules": { "type": "string", "nullable": true, "description": "Unique node identifier (set at runtime)." } + }, + "unit": { + "default": "unitless", + "rules": { "type": "string", "description": "Default measurement unit." } + }, + "logging": { + "logLevel": { + "default": "info", + "rules": { + "type": "enum", + "values": [ + { "value": "debug", "description": "Verbose diagnostic messages." }, + { "value": "info", "description": "General informational messages." }, + { "value": "warn", "description": "Warning messages." }, + { "value": "error", "description": "Error level messages only." } + ] + } + }, + "enabled": { + "default": true, + "rules": { "type": "boolean", "description": "Enable or disable logging." } + } + } + }, + "functionality": { + "softwareType": { + "default": "unknown", + "rules": { "type": "string", "description": "Software type identifier for parent-child registration." } + }, + "role": { + "default": "Generic EVOLV node", + "rules": { "type": "string", "description": "Describes the functional role of this node." } + }, + "positionVsParent": { + "default": "atEquipment", + "rules": { + "type": "enum", + "values": [ + { "value": "upstream", "description": "Upstream of parent equipment." }, + { "value": "atEquipment", "description": "At equipment level." }, + { "value": "downstream", "description": "Downstream of parent equipment." } + ] + } + } + }, + "asset": { + "uuid": { + "default": null, + "rules": { "type": "string", "nullable": true, "description": "Asset UUID from asset management system." } + }, + "tagCode": { + "default": null, + "rules": { "type": "string", "nullable": true, "description": "Asset tag code." } + }, + "supplier": { + "default": "Unknown", + "rules": { "type": "string", "description": "Equipment supplier." } + }, + "category": { + "default": "sensor", + "rules": { "type": "string", "description": "Asset category." } + }, + "type": { + "default": "Unknown", + "rules": { "type": "string", "description": "Asset type." } + }, + "model": { + "default": "Unknown", + "rules": { "type": "string", "description": "Equipment model." } + }, + "unit": { + "default": "unitless", + "rules": { "type": "string", "description": "Asset measurement unit." } + } + } +} diff --git a/src/configs/index.js b/src/configs/index.js index ff85a92..3adb603 100644 --- a/src/configs/index.js +++ b/src/configs/index.js @@ -47,6 +47,69 @@ class ConfigManager { return fs.existsSync(configPath); } + /** + * Build a runtime config by merging base schema + node schema + UI overrides. + * Eliminates the need for each nodeClass to manually construct general/asset/functionality sections. + * + * @param {string} nodeName - Node type name (e.g., 'valve', 'measurement') + * @param {object} uiConfig - Raw config from Node-RED UI + * @param {string} nodeId - Node-RED node ID (from node.id) + * @param {object} [domainConfig={}] - Domain-specific config sections (e.g., { scaling: {...}, smoothing: {...} }) + * @returns {object} Merged runtime config + * + * @example + * const cfgMgr = new ConfigManager(); + * const config = cfgMgr.buildConfig('measurement', uiConfig, node.id, { + * scaling: { enabled: uiConfig.scaling, inputMin: uiConfig.i_min, ... }, + * smoothing: { smoothWindow: uiConfig.count, ... } + * }); + */ + buildConfig(nodeName, uiConfig, nodeId, domainConfig = {}) { + // Build base sections from UI config (common to ALL nodes) + const config = { + general: { + name: uiConfig.name || nodeName, + id: nodeId, + unit: uiConfig.unit || 'unitless', + logging: { + enabled: uiConfig.enableLog !== undefined ? uiConfig.enableLog : true, + logLevel: uiConfig.logLevel || 'info' + } + }, + functionality: { + softwareType: nodeName, + positionVsParent: uiConfig.positionVsParent || 'atEquipment', + distance: uiConfig.hasDistance ? uiConfig.distance : undefined + } + }; + + // Add asset section if UI provides asset fields + if (uiConfig.supplier || uiConfig.category || uiConfig.assetType || uiConfig.model) { + config.asset = { + uuid: uiConfig.uuid || uiConfig.assetUuid || null, + tagCode: uiConfig.tagCode || uiConfig.assetTagCode || null, + supplier: uiConfig.supplier || 'Unknown', + category: uiConfig.category || 'sensor', + type: uiConfig.assetType || 'Unknown', + model: uiConfig.model || 'Unknown', + unit: uiConfig.unit || 'unitless' + }; + } + + // Merge domain-specific sections + Object.assign(config, domainConfig); + + return config; + } + + /** + * Get the base config schema (shared across all nodes). + * @returns {object} Base config schema + */ + getBaseConfig() { + return this.getConfig('baseConfig'); + } + createEndpoint(nodeName) { try { // Load the config for this node