diff --git a/datasets/assetData/machine.json b/datasets/assetData/machine.json deleted file mode 100644 index 04c8563..0000000 --- a/datasets/assetData/machine.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "id": "machine", - "label": "machine", - "softwareType": "machine", - "suppliers": [ - { - "id": "hidrostal", - "name": "Hidrostal", - "types": [ - { - "id": "pump-centrifugal", - "name": "Centrifugal", - "models": [ - { "id": "hidrostal-H05K-S03R", "name": "hidrostal-H05K-S03R", "units": ["l/s","m3/h"] }, - { "id": "hidrostal-C5-D03R-SHN1", "name": "hidrostal-C5-D03R-SHN1", "units": ["l/s"] } - ] - } - ] - } - ] -} \ No newline at end of file diff --git a/datasets/assetData/rotatingmachine.json b/datasets/assetData/rotatingmachine.json new file mode 100644 index 0000000..8c3e9c9 --- /dev/null +++ b/datasets/assetData/rotatingmachine.json @@ -0,0 +1,34 @@ +{ + "id": "rotatingmachine", + "label": "rotatingMachine", + "softwareType": "rotatingmachine", + "suppliers": [ + { + "id": "hidrostal", + "name": "Hidrostal", + "types": [ + { + "id": "pump-centrifugal", + "name": "Centrifugal", + "models": [ + { + "id": "hidrostal-H05K-S03R", + "name": "hidrostal-H05K-S03R", + "units": [ + "l/s", + "m3/h" + ] + }, + { + "id": "hidrostal-C5-D03R-SHN1", + "name": "hidrostal-C5-D03R-SHN1", + "units": [ + "l/s" + ] + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/src/configs/index.js b/src/configs/index.js index ed879ca..65bb4a3 100644 --- a/src/configs/index.js +++ b/src/configs/index.js @@ -117,18 +117,24 @@ class ConfigManager { } }; - // 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' - }; - } + // 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: diff --git a/src/configs/machineGroupControl.json b/src/configs/machineGroupControl.json index d8fc8f6..e3e760a 100644 --- a/src/configs/machineGroupControl.json +++ b/src/configs/machineGroupControl.json @@ -91,7 +91,55 @@ ], "description": "Defines the position of the measurement relative to its parent equipment or system." } + }, + "distance": { + "default": null, + "rules": { + "type": "number", + "nullable": true, + "description": "Optional spatial offset from the parent equipment reference. Populated from the editor when hasDistance is enabled; null otherwise." } + }, + "distanceUnit": { + "default": "m", + "rules": { + "type": "string", + "description": "Unit for the functionality.distance offset (e.g. 'm', 'cm')." + } + }, + "distanceDescription": { + "default": "", + "rules": { + "type": "string", + "description": "Free-text description of what the distance offset represents." + } + } + }, + "output": { + "process": { + "default": "process", + "rules": { + "type": "enum", + "values": [ + { "value": "process", "description": "Delta-compressed process message (default)." }, + { "value": "json", "description": "Raw JSON payload." }, + { "value": "csv", "description": "CSV-formatted payload." } + ], + "description": "Format of the process payload emitted on output port 0." + } + }, + "dbase": { + "default": "influxdb", + "rules": { + "type": "enum", + "values": [ + { "value": "influxdb", "description": "InfluxDB line-protocol payload (default)." }, + { "value": "json", "description": "Raw JSON payload." }, + { "value": "csv", "description": "CSV-formatted payload." } + ], + "description": "Format of the telemetry payload emitted on output port 1." + } + } }, "mode": { "current": { @@ -107,10 +155,6 @@ "value": "priorityControl", "description": "Machines are controlled sequentially from minimum to maximum output until each is maxed out, then additional machines are added." }, - { - "value": "prioritypercentagecontrol", - "description": "Machines are controlled sequentially from minimum to maximum output until each is maxed out, then additional machines are added based on a percentage of the total demand." - }, { "value": "maintenance", "description": "The group is in maintenance mode with limited actions (monitoring only)." @@ -140,14 +184,6 @@ "description": "Actions allowed in priorityControl mode." } }, - "prioritypercentagecontrol": { - "default": ["statusCheck", "execSequentialControl", "balanceLoad", "emergencyStop"], - "rules": { - "type": "set", - "itemType": "string", - "description": "Actions allowed in manualOverride mode." - } - }, "maintenance": { "default": ["statusCheck"], "rules": { @@ -180,37 +216,10 @@ "itemType": "string", "description": "Command sources allowed in priorityControl mode." } - }, - "prioritypercentagecontrol": { - "default": ["parent", "GUI", "physical", "API"], - "rules": { - "type": "set", - "itemType": "string", - "description": "Command sources allowed " - } } }, "description": "Specifies the valid command sources recognized by the machine group controller for each mode." } } - }, - "scaling": { - "current": { - "default": "normalized", - "rules": { - "type": "enum", - "values": [ - { - "value": "normalized", - "description": "Scales the demand between 0–100% of the total flow capacity, interpolating to calculate the effective demand." - }, - { - "value": "absolute", - "description": "Uses the absolute demand value directly, capped between the min and max machine flow capacities." - } - ], - "description": "The scaling mode for demand calculations." - } - } } } diff --git a/src/configs/measurement.json b/src/configs/measurement.json index 085c5c9..c8eb188 100644 --- a/src/configs/measurement.json +++ b/src/configs/measurement.json @@ -96,10 +96,37 @@ "default": null, "rules": { "type": "number", + "nullable": true, "description": "Defines the position of the measurement relative to its parent equipment or system." } } }, + "output": { + "process": { + "default": "process", + "rules": { + "type": "enum", + "values": [ + { "value": "process", "description": "Delta-compressed process message (default)." }, + { "value": "json", "description": "Raw JSON payload." }, + { "value": "csv", "description": "CSV-formatted payload." } + ], + "description": "Format of the process payload emitted on output port 0." + } + }, + "dbase": { + "default": "influxdb", + "rules": { + "type": "enum", + "values": [ + { "value": "influxdb", "description": "InfluxDB line-protocol payload (default)." }, + { "value": "json", "description": "Raw JSON payload." }, + { "value": "csv", "description": "CSV-formatted payload." } + ], + "description": "Format of the telemetry payload emitted on output port 1." + } + } + }, "asset": { "uuid": { "default": null, diff --git a/src/menu/asset.js b/src/menu/asset.js index a07a1f9..47bdac3 100644 --- a/src/menu/asset.js +++ b/src/menu/asset.js @@ -55,7 +55,17 @@ class AssetMenu { } } - return keys[0]; + // Previously fell back to keys[0] (alphabetically first category), + // which meant a softwareType mismatch silently showed the wrong asset + // tree — e.g. rotatingMachine (softwareType='rotatingmachine') with + // no matching registry file saw 'diffuser' models in the dropdown. + // Return null so the menu renders empty and the operator sees a clear + // 'No suppliers available' placeholder instead of a wrong category. + console.warn( + `[AssetMenu] No asset category matches softwareType='${this.softwareType}' or nodeName='${nodeName}'. ` + + `Available categories: [${keys.join(', ')}]. Menu will render empty.` + ); + return null; } getAllMenuData(nodeName) { @@ -253,6 +263,26 @@ class AssetMenu { } const suppliers = activeCategory ? activeCategory.suppliers : []; + + // The save handler intentionally discards node.supplier / node.assetType + // (denormalized copies of registry data — only node.model + node.unit + // are persisted identity). So on reopen we re-derive them from the + // saved model id by walking the registry tree. Without this the + // cascade always boots at "Select..." even when a model is saved. + if (node.model && (!node.supplier || !node.assetType)) { + for (const supplier of suppliers) { + const match = (supplier.types || []).find((type) => + (type.models || []).some((model) => + String(model.id || model.name) === String(node.model)) + ); + if (match) { + node.supplier = supplier.id || supplier.name; + node.assetType = match.id || match.name; + break; + } + } + } + populate( elems.supplier, suppliers, diff --git a/src/predict/predict_class.js b/src/predict/predict_class.js index 829a842..c473f68 100644 --- a/src/predict/predict_class.js +++ b/src/predict/predict_class.js @@ -71,14 +71,22 @@ class Predict { // Capture share-source BEFORE config validation strips it (ConfigUtils // mutates the input config to drop unknown keys, which would remove // shareInputsFrom because it's not in predictConfig.json's schema). + // Detach on a shallow clone so validateSchema doesn't see the key at all + // — leaving it on the input would emit a `[interpolation] Unknown key + // 'shareInputsFrom'` warning per group-predictor on every curve refresh. const _sharedSource = (config && config.shareInputsFrom instanceof Predict) ? config.shareInputsFrom : null; + let _initConfig = config; + if (_initConfig && 'shareInputsFrom' in _initConfig) { + _initConfig = { ..._initConfig }; + delete _initConfig.shareInputsFrom; + } // Initialize dependencies this.emitter = new EventEmitter(); // Own EventEmitter this.configUtils = new ConfigUtils(defaultConfig); - this.config = this.configUtils.initConfig(config); + this.config = this.configUtils.initConfig(_initConfig); // Init after config is set this.logger = new Logger(this.config.general.logging.enabled,this.config.general.logging.logLevel, this.config.general.name);