Compare commits

..

1 Commits

Author SHA1 Message Date
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
7 changed files with 167 additions and 74 deletions

View File

@@ -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"] }
]
}
]
}
]
}

View File

@@ -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"
]
}
]
}
]
}
]
}

View File

@@ -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:

View File

@@ -91,6 +91,54 @@
],
"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": {
@@ -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 0100% 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."
}
}
}
}

View File

@@ -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,

View File

@@ -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,

View File

@@ -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);