From f8f71a4f1c770ad2a805c34c3041b82f0e085586 Mon Sep 17 00:00:00 2001 From: znetsixe Date: Thu, 14 May 2026 22:51:57 +0200 Subject: [PATCH] schema + asset menu fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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) --- datasets/assetData/machine.json | 21 ------ datasets/assetData/rotatingmachine.json | 34 ++++++++++ src/configs/index.js | 30 +++++---- src/configs/machineGroupControl.json | 87 ++++++++++++++----------- src/configs/measurement.json | 27 ++++++++ src/menu/asset.js | 32 ++++++++- src/predict/predict_class.js | 10 ++- 7 files changed, 167 insertions(+), 74 deletions(-) delete mode 100644 datasets/assetData/machine.json create mode 100644 datasets/assetData/rotatingmachine.json 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);