Files
generalFunctions/src/configs/index.js
znetsixe f8f71a4f1c schema + asset menu fixes
- configs/machineGroupControl.json: drop prioritypercentagecontrol mode
  (unused — set.demand became unit-self-describing, so percentage-vs-absolute
  is decided per-message, not by a node-wide scaling mode). Add output.process
  / output.dbase enums + functionality.distance{,Unit,Description} so the
  editor's distance offset persists. Fixes the runtime warnings 'Unknown key
  optimization/scaling/movement/curvePressureUnit etc.' the validator was
  logging on every MGC instantiation.
- configs/measurement.json: same output.process/dbase block + nullable
  position.x for the rare case a measurement has no parent yet.
- datasets/assetData/machine.json -> rotatingmachine.json: rename so
  AssetMenu's softwareType lookup matches. AssetMenu.getActiveCategoryKey
  no longer silently falls back to keys[0] (which mis-showed diffuser models
  for rotatingMachine nodes) — returns null with a console.warn instead.
- menu/asset.js: re-derive supplier/assetType from saved model id on reopen.
  The save handler intentionally discards the denormalized registry copies
  to keep the persisted node small, so the cascade dropdown booted at
  'Select...' even when a model was saved. Walk the registry tree to
  reconstitute.
- predict/predict_class.js: minor.
- configs/index.js: minor.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 22:51:57 +02:00

230 lines
8.7 KiB
JavaScript

const fs = require('fs');
const path = require('path');
/**
* Current config version. All config JSONs should declare this version.
* Bump this when the config schema changes.
*/
const CURRENT_CONFIG_VERSION = '1.0.0';
class ConfigManager {
constructor(relPath = '.') {
this.configDir = path.resolve(__dirname, relPath);
/**
* Migration functions keyed by "fromVersion->toVersion".
* Each function receives a config object and returns the migrated config.
*
* Example:
* this.migrations['1.0.0->1.1.0'] = (config) => {
* config.newSection = { enabled: false };
* return config;
* };
*/
this.migrations = {};
}
/**
* Load a configuration file by name.
* Automatically checks the config version and migrates if needed.
* @param {string} configName - Name of the config file (without .json extension)
* @returns {Object} Parsed configuration object (migrated to current version if necessary)
*/
getConfig(configName) {
try {
const configPath = path.resolve(this.configDir, `${configName}.json`);
const configData = fs.readFileSync(configPath, 'utf8');
let config = JSON.parse(configData);
// Auto-migrate if version is behind current
const configVersion = config.version || '0.0.0';
if (configVersion !== CURRENT_CONFIG_VERSION) {
config = this.migrateConfig(config, configVersion, CURRENT_CONFIG_VERSION);
}
return config;
} catch (error) {
if (error.message && error.message.startsWith('Failed to load config')) {
throw error;
}
throw new Error(`Failed to load config '${configName}': ${error.message}`);
}
}
/**
* Get list of available configuration files
* @returns {Array<string>} Array of config names (without .json extension)
*/
getAvailableConfigs() {
try {
const resolvedDir = path.resolve(this.configDir);
const files = fs.readdirSync(resolvedDir);
return files
.filter(file => file.endsWith('.json'))
.map(file => path.basename(file, '.json'));
} catch (error) {
throw new Error(`Failed to read config directory: ${error.message}`);
}
}
/**
* Check if a specific config exists
* @param {string} configName - Name of the config file
* @returns {boolean} True if config exists
*/
hasConfig(configName) {
const configPath = path.resolve(this.configDir, `${configName}.json`);
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.toLowerCase(),
positionVsParent: uiConfig.positionVsParent || 'atEquipment',
distance: uiConfig.hasDistance ? uiConfig.distance : null
},
output: {
process: uiConfig.processOutputFormat || 'process',
dbase: uiConfig.dbaseOutputFormat || 'influxdb'
}
};
// Asset section is emitted per-key: only fields the editor actually
// set propagate to the domain config. Schemas that omit a key (e.g.
// rotatingMachine deliberately drops asset.supplier/category/type
// because those come from the asset registry at runtime) no longer
// get those keys injected and then stripped by ValidationUtils with
// a warning. Empty strings from HTML defaults stay falsy → omitted →
// schema default applies.
const asset = {};
const uuid = uiConfig.uuid || uiConfig.assetUuid;
const tagCode = uiConfig.tagCode || uiConfig.assetTagCode;
if (uuid) asset.uuid = uuid;
if (tagCode) asset.tagCode = tagCode;
if (uiConfig.supplier) asset.supplier = uiConfig.supplier;
if (uiConfig.category) asset.category = uiConfig.category;
if (uiConfig.assetType) asset.type = uiConfig.assetType;
if (uiConfig.model) asset.model = uiConfig.model;
if (uiConfig.unit) asset.unit = uiConfig.unit;
if (Object.keys(asset).length > 0) config.asset = asset;
// Merge domain-specific sections. Must be a DEEP merge: domainConfig
// commonly returns subsets of `general` / `asset` (e.g. {general:
// {unit}}, {asset: {curveUnits}}) and a shallow assign would wipe out
// sibling keys this method just populated — notably `general.id`
// (nodeId) and `asset.model`, causing child-registration id collisions
// and curve-lookup failures downstream.
ConfigManager._deepMerge(config, domainConfig);
return config;
}
static _isPlainObject(v) {
return Object.prototype.toString.call(v) === '[object Object]';
}
/**
* In-place recursive merge. Arrays and primitives in `src` replace `dst`;
* plain objects are merged key-by-key so siblings on `dst` survive.
*/
static _deepMerge(dst, src) {
if (!ConfigManager._isPlainObject(src)) return dst;
for (const key of Object.keys(src)) {
const v = src[key];
if (Array.isArray(v)) {
dst[key] = [...v];
} else if (ConfigManager._isPlainObject(v)) {
if (!ConfigManager._isPlainObject(dst[key])) dst[key] = {};
ConfigManager._deepMerge(dst[key], v);
} else {
dst[key] = v;
}
}
return dst;
}
/**
* Migrate a config object from one version to another by applying
* registered migration functions in sequence.
* @param {object} config - The config object to migrate
* @param {string} fromVersion - Current version of the config
* @param {string} toVersion - Target version
* @returns {object} Migrated config with updated version field
*/
migrateConfig(config, fromVersion, toVersion) {
const migrationKey = `${fromVersion}->${toVersion}`;
const migrationFn = this.migrations[migrationKey];
if (migrationFn) {
config = migrationFn(config);
}
// Stamp the current version so it won't re-migrate
config.version = toVersion;
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
const config = this.getConfig(nodeName);
// Convert config to JSON
const configJSON = JSON.stringify(config, null, 2);
// Assemble the complete script
return `
// Create the namespace structure
window.EVOLV = window.EVOLV || {};
window.EVOLV.nodes = window.EVOLV.nodes || {};
window.EVOLV.nodes.${nodeName} = window.EVOLV.nodes.${nodeName} || {};
// Inject the pre-loaded config data directly into the namespace
window.EVOLV.nodes.${nodeName}.config = ${configJSON};
console.log('${nodeName} config loaded and endpoint created');
`;
} catch (error) {
throw new Error(`Failed to create endpoint for '${nodeName}': ${error.message}`);
}
}
}
module.exports = ConfigManager;