Compare commits
24 Commits
108d2e23ca
...
fix/emerge
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ea33b3bba3 | ||
|
|
f363ee53ef | ||
|
|
4cf46f33c9 | ||
|
|
7b9fdd7342 | ||
|
|
bb986c2dc8 | ||
|
|
46dd2ca37a | ||
|
|
ccfa90394b | ||
|
|
6b2a8239f2 | ||
|
|
33f3c2ef61 | ||
|
|
b5137ba9c2 | ||
|
|
405be33626 | ||
|
|
c63701db38 | ||
|
|
e236cccfd6 | ||
|
|
99b45c87e4 | ||
|
|
0a98b12224 | ||
|
|
b6d268659a | ||
|
|
303dfc477d | ||
|
|
ac40a93ef1 | ||
|
|
a8fb56bfb8 | ||
|
|
d7cc6a4a8b | ||
|
|
37e6523c55 | ||
| 5a14f44fdd | |||
|
|
c081acae4e | ||
| 08185243bc |
345
examples/01 - Basic Manual Control.json
Normal file
345
examples/01 - Basic Manual Control.json
Normal file
@@ -0,0 +1,345 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "rm_basic_tab",
|
||||||
|
"type": "tab",
|
||||||
|
"label": "RotatingMachine - Basic Manual Control",
|
||||||
|
"disabled": false,
|
||||||
|
"info": "Demonstrates basic manual control of a single rotatingMachine using inject nodes only. No dashboard dependencies."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "rm_basic_comment_title",
|
||||||
|
"type": "comment",
|
||||||
|
"z": "rm_basic_tab",
|
||||||
|
"name": "RotatingMachine - Basic Manual Control\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\nDemonstrates startup/shutdown sequences, speed setpoint\ncontrol, and pressure simulation for a single pump.\n\nPrerequisites: EVOLV package installed.",
|
||||||
|
"info": "",
|
||||||
|
"x": 360,
|
||||||
|
"y": 40,
|
||||||
|
"wires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "rm_basic_comment_howto",
|
||||||
|
"type": "comment",
|
||||||
|
"z": "rm_basic_tab",
|
||||||
|
"name": "HOW TO USE:\n1. Deploy flow\n2. Click 'Set virtualControl' to enable manual control\n3. Click 'Startup' to run the startup sequence\n4. Click 'Set 60%' to move to 60% speed\n5. Inject BOTH pressure values to see flow/power predictions\n6. Click 'Shutdown' when done\n\nNOTE: Output uses delta compression - only changed\nfields are sent each tick. The formatter merges\ndeltas into a running cache for display.\n\nIMPORTANT: Flow and power predictions require at least\none pressure value to be injected. Without pressure,\npredictions use fDimension=0 (unrealistic values).\nOutput keys use 4-segment format:\ntype.variant.position.childId (e.g. flow.predicted.downstream.default)",
|
||||||
|
"info": "",
|
||||||
|
"x": 360,
|
||||||
|
"y": 100,
|
||||||
|
"wires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "rm_basic_node",
|
||||||
|
"type": "rotatingMachine",
|
||||||
|
"z": "rm_basic_tab",
|
||||||
|
"name": "Pump 1",
|
||||||
|
"speed": "1",
|
||||||
|
"startup": "3",
|
||||||
|
"warmup": "2",
|
||||||
|
"shutdown": "3",
|
||||||
|
"cooldown": "2",
|
||||||
|
"movementMode": "staticspeed",
|
||||||
|
"machineCurve": "",
|
||||||
|
"uuid": "example-pump-001",
|
||||||
|
"supplier": "hidrostal",
|
||||||
|
"category": "pump",
|
||||||
|
"assetType": "pump-centrifugal",
|
||||||
|
"model": "hidrostal-H05K-S03R",
|
||||||
|
"unit": "m3/h",
|
||||||
|
"curvePressureUnit": "mbar",
|
||||||
|
"curveFlowUnit": "m3/h",
|
||||||
|
"curvePowerUnit": "kW",
|
||||||
|
"curveControlUnit": "%",
|
||||||
|
"enableLog": true,
|
||||||
|
"logLevel": "info",
|
||||||
|
"positionVsParent": "atEquipment",
|
||||||
|
"positionIcon": "",
|
||||||
|
"hasDistance": false,
|
||||||
|
"distance": "",
|
||||||
|
"distanceUnit": "m",
|
||||||
|
"distanceDescription": "",
|
||||||
|
"x": 560,
|
||||||
|
"y": 340,
|
||||||
|
"wires": [
|
||||||
|
["rm_basic_format_output"],
|
||||||
|
["rm_basic_debug_port1"],
|
||||||
|
["rm_basic_debug_port2"]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "rm_basic_inject_mode",
|
||||||
|
"type": "inject",
|
||||||
|
"z": "rm_basic_tab",
|
||||||
|
"name": "Set virtualControl",
|
||||||
|
"props": [
|
||||||
|
{ "p": "topic", "vt": "str" },
|
||||||
|
{ "p": "payload", "v": "virtualControl", "vt": "str" }
|
||||||
|
],
|
||||||
|
"topic": "setMode",
|
||||||
|
"repeat": "",
|
||||||
|
"crontab": "",
|
||||||
|
"once": false,
|
||||||
|
"onceDelay": "",
|
||||||
|
"x": 190,
|
||||||
|
"y": 200,
|
||||||
|
"wires": [["rm_basic_node"]]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "rm_basic_inject_auto",
|
||||||
|
"type": "inject",
|
||||||
|
"z": "rm_basic_tab",
|
||||||
|
"name": "Set auto mode",
|
||||||
|
"props": [
|
||||||
|
{ "p": "topic", "vt": "str" },
|
||||||
|
{ "p": "payload", "v": "auto", "vt": "str" }
|
||||||
|
],
|
||||||
|
"topic": "setMode",
|
||||||
|
"repeat": "",
|
||||||
|
"crontab": "",
|
||||||
|
"once": false,
|
||||||
|
"onceDelay": "",
|
||||||
|
"x": 180,
|
||||||
|
"y": 240,
|
||||||
|
"wires": [["rm_basic_node"]]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "rm_basic_inject_startup",
|
||||||
|
"type": "inject",
|
||||||
|
"z": "rm_basic_tab",
|
||||||
|
"name": "Startup",
|
||||||
|
"props": [
|
||||||
|
{ "p": "topic", "vt": "str" },
|
||||||
|
{ "p": "payload", "v": "{\"source\":\"GUI\",\"action\":\"execSequence\",\"parameter\":\"startup\"}", "vt": "json" }
|
||||||
|
],
|
||||||
|
"topic": "execSequence",
|
||||||
|
"repeat": "",
|
||||||
|
"crontab": "",
|
||||||
|
"once": false,
|
||||||
|
"onceDelay": "",
|
||||||
|
"x": 170,
|
||||||
|
"y": 300,
|
||||||
|
"wires": [["rm_basic_node"]]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "rm_basic_inject_shutdown",
|
||||||
|
"type": "inject",
|
||||||
|
"z": "rm_basic_tab",
|
||||||
|
"name": "Shutdown",
|
||||||
|
"props": [
|
||||||
|
{ "p": "topic", "vt": "str" },
|
||||||
|
{ "p": "payload", "v": "{\"source\":\"GUI\",\"action\":\"execSequence\",\"parameter\":\"shutdown\"}", "vt": "json" }
|
||||||
|
],
|
||||||
|
"topic": "execSequence",
|
||||||
|
"repeat": "",
|
||||||
|
"crontab": "",
|
||||||
|
"once": false,
|
||||||
|
"onceDelay": "",
|
||||||
|
"x": 170,
|
||||||
|
"y": 340,
|
||||||
|
"wires": [["rm_basic_node"]]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "rm_basic_inject_emergency",
|
||||||
|
"type": "inject",
|
||||||
|
"z": "rm_basic_tab",
|
||||||
|
"name": "Emergency Stop",
|
||||||
|
"props": [
|
||||||
|
{ "p": "topic", "vt": "str" },
|
||||||
|
{ "p": "payload", "v": "{\"source\":\"GUI\",\"action\":\"emergencystop\"}", "vt": "json" }
|
||||||
|
],
|
||||||
|
"topic": "emergencystop",
|
||||||
|
"repeat": "",
|
||||||
|
"crontab": "",
|
||||||
|
"once": false,
|
||||||
|
"onceDelay": "",
|
||||||
|
"x": 190,
|
||||||
|
"y": 380,
|
||||||
|
"wires": [["rm_basic_node"]]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "rm_basic_inject_setpoint60",
|
||||||
|
"type": "inject",
|
||||||
|
"z": "rm_basic_tab",
|
||||||
|
"name": "Set 60%",
|
||||||
|
"props": [
|
||||||
|
{ "p": "topic", "vt": "str" },
|
||||||
|
{ "p": "payload", "v": "{\"source\":\"GUI\",\"action\":\"execMovement\",\"setpoint\":60}", "vt": "json" }
|
||||||
|
],
|
||||||
|
"topic": "execMovement",
|
||||||
|
"repeat": "",
|
||||||
|
"crontab": "",
|
||||||
|
"once": false,
|
||||||
|
"onceDelay": "",
|
||||||
|
"x": 170,
|
||||||
|
"y": 440,
|
||||||
|
"wires": [["rm_basic_node"]]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "rm_basic_inject_setpoint30",
|
||||||
|
"type": "inject",
|
||||||
|
"z": "rm_basic_tab",
|
||||||
|
"name": "Set 30%",
|
||||||
|
"props": [
|
||||||
|
{ "p": "topic", "vt": "str" },
|
||||||
|
{ "p": "payload", "v": "{\"source\":\"GUI\",\"action\":\"execMovement\",\"setpoint\":30}", "vt": "json" }
|
||||||
|
],
|
||||||
|
"topic": "execMovement",
|
||||||
|
"repeat": "",
|
||||||
|
"crontab": "",
|
||||||
|
"once": false,
|
||||||
|
"onceDelay": "",
|
||||||
|
"x": 170,
|
||||||
|
"y": 480,
|
||||||
|
"wires": [["rm_basic_node"]]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "rm_basic_inject_setpoint100",
|
||||||
|
"type": "inject",
|
||||||
|
"z": "rm_basic_tab",
|
||||||
|
"name": "Set 100%",
|
||||||
|
"props": [
|
||||||
|
{ "p": "topic", "vt": "str" },
|
||||||
|
{ "p": "payload", "v": "{\"source\":\"GUI\",\"action\":\"execMovement\",\"setpoint\":100}", "vt": "json" }
|
||||||
|
],
|
||||||
|
"topic": "execMovement",
|
||||||
|
"repeat": "",
|
||||||
|
"crontab": "",
|
||||||
|
"once": false,
|
||||||
|
"onceDelay": "",
|
||||||
|
"x": 170,
|
||||||
|
"y": 520,
|
||||||
|
"wires": [["rm_basic_node"]]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "rm_basic_inject_pressure_down",
|
||||||
|
"type": "inject",
|
||||||
|
"z": "rm_basic_tab",
|
||||||
|
"name": "Sim downstream 1100 mbar",
|
||||||
|
"props": [
|
||||||
|
{ "p": "topic", "vt": "str" },
|
||||||
|
{ "p": "payload", "v": "{\"type\":\"pressure\",\"position\":\"downstream\",\"value\":1100,\"unit\":\"mbar\"}", "vt": "json" }
|
||||||
|
],
|
||||||
|
"topic": "simulateMeasurement",
|
||||||
|
"repeat": "",
|
||||||
|
"crontab": "",
|
||||||
|
"once": false,
|
||||||
|
"onceDelay": "",
|
||||||
|
"x": 230,
|
||||||
|
"y": 600,
|
||||||
|
"wires": [["rm_basic_node"]]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "rm_basic_inject_pressure_up",
|
||||||
|
"type": "inject",
|
||||||
|
"z": "rm_basic_tab",
|
||||||
|
"name": "Sim upstream 200 mbar",
|
||||||
|
"props": [
|
||||||
|
{ "p": "topic", "vt": "str" },
|
||||||
|
{ "p": "payload", "v": "{\"type\":\"pressure\",\"position\":\"upstream\",\"value\":200,\"unit\":\"mbar\"}", "vt": "json" }
|
||||||
|
],
|
||||||
|
"topic": "simulateMeasurement",
|
||||||
|
"repeat": "",
|
||||||
|
"crontab": "",
|
||||||
|
"once": false,
|
||||||
|
"onceDelay": "",
|
||||||
|
"x": 220,
|
||||||
|
"y": 640,
|
||||||
|
"wires": [["rm_basic_node"]]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "rm_basic_inject_maintenance",
|
||||||
|
"type": "inject",
|
||||||
|
"z": "rm_basic_tab",
|
||||||
|
"name": "Enter Maintenance",
|
||||||
|
"props": [
|
||||||
|
{ "p": "topic", "vt": "str" },
|
||||||
|
{ "p": "payload", "v": "{\"source\":\"GUI\",\"action\":\"execSequence\",\"parameter\":\"entermaintenance\"}", "vt": "json" }
|
||||||
|
],
|
||||||
|
"topic": "execSequence",
|
||||||
|
"repeat": "",
|
||||||
|
"crontab": "",
|
||||||
|
"once": false,
|
||||||
|
"onceDelay": "",
|
||||||
|
"x": 200,
|
||||||
|
"y": 700,
|
||||||
|
"wires": [["rm_basic_node"]]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "rm_basic_inject_leavemaint",
|
||||||
|
"type": "inject",
|
||||||
|
"z": "rm_basic_tab",
|
||||||
|
"name": "Leave Maintenance",
|
||||||
|
"props": [
|
||||||
|
{ "p": "topic", "vt": "str" },
|
||||||
|
{ "p": "payload", "v": "{\"source\":\"GUI\",\"action\":\"execSequence\",\"parameter\":\"leavemaintenance\"}", "vt": "json" }
|
||||||
|
],
|
||||||
|
"topic": "execSequence",
|
||||||
|
"repeat": "",
|
||||||
|
"crontab": "",
|
||||||
|
"once": false,
|
||||||
|
"onceDelay": "",
|
||||||
|
"x": 200,
|
||||||
|
"y": 740,
|
||||||
|
"wires": [["rm_basic_node"]]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "rm_basic_format_output",
|
||||||
|
"type": "function",
|
||||||
|
"z": "rm_basic_tab",
|
||||||
|
"name": "Merge deltas and format",
|
||||||
|
"func": "const p = msg.payload || {};\nconst cache = context.get('c') || {};\nObject.assign(cache, p);\ncontext.set('c', cache);\nfunction find(prefix) {\n for (var k in cache) { if (k.indexOf(prefix) === 0) return cache[k]; }\n return null;\n}\nconst fl = find('flow.predicted.downstream.');\nconst pw = find('power.predicted.atequipment.');\nconst pD = find('pressure.measured.downstream.');\nconst pU = find('pressure.measured.upstream.');\nmsg.payload = {\n state: cache.state || 'idle',\n mode: cache.mode || 'auto',\n ctrl: cache.ctrl != null ? Number(cache.ctrl).toFixed(1) + '%' : 'n/a',\n flow: fl != null ? Number(fl).toFixed(2) + ' m3/h' : 'n/a',\n power: pw != null ? Number(pw).toFixed(2) + ' kW' : 'n/a',\n NCog: cache.NCog != null ? Number(cache.NCog).toFixed(1) + '%' : 'n/a',\n pDown: pD != null ? Number(pD).toFixed(0) + ' mbar' : 'n/a',\n pUp: pU != null ? Number(pU).toFixed(0) + ' mbar' : 'n/a',\n runtime: cache.runtime != null ? Number(cache.runtime).toFixed(3) + ' h' : '0'\n};\nreturn msg;",
|
||||||
|
"outputs": 1,
|
||||||
|
"noerr": 0,
|
||||||
|
"initialize": "",
|
||||||
|
"finalize": "",
|
||||||
|
"libs": [],
|
||||||
|
"x": 790,
|
||||||
|
"y": 300,
|
||||||
|
"wires": [["rm_basic_debug_port0"]]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "rm_basic_debug_port0",
|
||||||
|
"type": "debug",
|
||||||
|
"z": "rm_basic_tab",
|
||||||
|
"name": "Port 0: Process Data",
|
||||||
|
"active": true,
|
||||||
|
"tosidebar": true,
|
||||||
|
"console": false,
|
||||||
|
"tostatus": true,
|
||||||
|
"complete": "payload",
|
||||||
|
"targetType": "msg",
|
||||||
|
"statusVal": "payload.state",
|
||||||
|
"statusType": "auto",
|
||||||
|
"x": 1020,
|
||||||
|
"y": 300,
|
||||||
|
"wires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "rm_basic_debug_port1",
|
||||||
|
"type": "debug",
|
||||||
|
"z": "rm_basic_tab",
|
||||||
|
"name": "Port 1: InfluxDB Telemetry",
|
||||||
|
"active": false,
|
||||||
|
"tosidebar": true,
|
||||||
|
"console": false,
|
||||||
|
"tostatus": false,
|
||||||
|
"complete": "true",
|
||||||
|
"targetType": "full",
|
||||||
|
"x": 1040,
|
||||||
|
"y": 360,
|
||||||
|
"wires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "rm_basic_debug_port2",
|
||||||
|
"type": "debug",
|
||||||
|
"z": "rm_basic_tab",
|
||||||
|
"name": "Port 2: Parent Registration",
|
||||||
|
"active": true,
|
||||||
|
"tosidebar": true,
|
||||||
|
"console": false,
|
||||||
|
"tostatus": false,
|
||||||
|
"complete": "true",
|
||||||
|
"targetType": "full",
|
||||||
|
"x": 1040,
|
||||||
|
"y": 420,
|
||||||
|
"wires": []
|
||||||
|
}
|
||||||
|
]
|
||||||
368
examples/02 - Integration with Machine Group.json
Normal file
368
examples/02 - Integration with Machine Group.json
Normal file
@@ -0,0 +1,368 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "rm_int_tab",
|
||||||
|
"type": "tab",
|
||||||
|
"label": "RotatingMachine - Integration with Machine Group",
|
||||||
|
"disabled": false,
|
||||||
|
"info": "Demonstrates a machineGroupControl parent with two rotatingMachine children and a measurement node."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "rm_int_comment_title",
|
||||||
|
"type": "comment",
|
||||||
|
"z": "rm_int_tab",
|
||||||
|
"name": "RotatingMachine - Integration with Parent\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\nShows how rotatingMachine registers with a\nmachineGroupControl parent via Port 2.\nAlso shows a measurement node providing\npressure data to a pump.\n\nPrerequisites: EVOLV package installed.",
|
||||||
|
"info": "",
|
||||||
|
"x": 380,
|
||||||
|
"y": 40,
|
||||||
|
"wires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "rm_int_comment_howto",
|
||||||
|
"type": "comment",
|
||||||
|
"z": "rm_int_tab",
|
||||||
|
"name": "HOW TO USE:\n1. Deploy flow - pumps auto-register with MGC via Port 2\n2. Set Pump 1 to virtualControl, then Startup\n3. Set speed setpoints on individual pumps\n4. Observe MGC aggregating child state on Port 0\n5. Inject pressure measurement to see curve predictions",
|
||||||
|
"info": "",
|
||||||
|
"x": 380,
|
||||||
|
"y": 110,
|
||||||
|
"wires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "rm_int_mgc",
|
||||||
|
"type": "machineGroupControl",
|
||||||
|
"z": "rm_int_tab",
|
||||||
|
"name": "Machine Group",
|
||||||
|
"enableLog": true,
|
||||||
|
"logLevel": "info",
|
||||||
|
"positionVsParent": "atEquipment",
|
||||||
|
"positionIcon": "",
|
||||||
|
"hasDistance": false,
|
||||||
|
"distance": "",
|
||||||
|
"distanceUnit": "m",
|
||||||
|
"x": 570,
|
||||||
|
"y": 300,
|
||||||
|
"wires": [
|
||||||
|
["rm_int_debug_mgc_port0"],
|
||||||
|
["rm_int_debug_mgc_port1"],
|
||||||
|
["rm_int_debug_mgc_port2"]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "rm_int_pump1",
|
||||||
|
"type": "rotatingMachine",
|
||||||
|
"z": "rm_int_tab",
|
||||||
|
"name": "Pump 1",
|
||||||
|
"speed": "1",
|
||||||
|
"startup": "2",
|
||||||
|
"warmup": "1",
|
||||||
|
"shutdown": "2",
|
||||||
|
"cooldown": "1",
|
||||||
|
"movementMode": "staticspeed",
|
||||||
|
"machineCurve": "",
|
||||||
|
"uuid": "example-pump-001",
|
||||||
|
"supplier": "hidrostal",
|
||||||
|
"category": "pump",
|
||||||
|
"assetType": "pump-centrifugal",
|
||||||
|
"model": "hidrostal-H05K-S03R",
|
||||||
|
"unit": "m3/h",
|
||||||
|
"curvePressureUnit": "mbar",
|
||||||
|
"curveFlowUnit": "m3/h",
|
||||||
|
"curvePowerUnit": "kW",
|
||||||
|
"curveControlUnit": "%",
|
||||||
|
"enableLog": true,
|
||||||
|
"logLevel": "info",
|
||||||
|
"positionVsParent": "atEquipment",
|
||||||
|
"positionIcon": "",
|
||||||
|
"hasDistance": false,
|
||||||
|
"distance": "",
|
||||||
|
"distanceUnit": "m",
|
||||||
|
"distanceDescription": "",
|
||||||
|
"x": 570,
|
||||||
|
"y": 480,
|
||||||
|
"wires": [
|
||||||
|
["rm_int_debug_p1_port0"],
|
||||||
|
[],
|
||||||
|
["rm_int_mgc"]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "rm_int_pump2",
|
||||||
|
"type": "rotatingMachine",
|
||||||
|
"z": "rm_int_tab",
|
||||||
|
"name": "Pump 2",
|
||||||
|
"speed": "1",
|
||||||
|
"startup": "2",
|
||||||
|
"warmup": "1",
|
||||||
|
"shutdown": "2",
|
||||||
|
"cooldown": "1",
|
||||||
|
"movementMode": "staticspeed",
|
||||||
|
"machineCurve": "",
|
||||||
|
"uuid": "example-pump-002",
|
||||||
|
"supplier": "hidrostal",
|
||||||
|
"category": "pump",
|
||||||
|
"assetType": "pump-centrifugal",
|
||||||
|
"model": "hidrostal-H05K-S03R",
|
||||||
|
"unit": "m3/h",
|
||||||
|
"curvePressureUnit": "mbar",
|
||||||
|
"curveFlowUnit": "m3/h",
|
||||||
|
"curvePowerUnit": "kW",
|
||||||
|
"curveControlUnit": "%",
|
||||||
|
"enableLog": true,
|
||||||
|
"logLevel": "info",
|
||||||
|
"positionVsParent": "atEquipment",
|
||||||
|
"positionIcon": "",
|
||||||
|
"hasDistance": false,
|
||||||
|
"distance": "",
|
||||||
|
"distanceUnit": "m",
|
||||||
|
"distanceDescription": "",
|
||||||
|
"x": 570,
|
||||||
|
"y": 620,
|
||||||
|
"wires": [
|
||||||
|
["rm_int_debug_p2_port0"],
|
||||||
|
[],
|
||||||
|
["rm_int_mgc"]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "rm_int_inject_mode_p1",
|
||||||
|
"type": "inject",
|
||||||
|
"z": "rm_int_tab",
|
||||||
|
"name": "P1: virtualControl",
|
||||||
|
"props": [
|
||||||
|
{ "p": "topic", "vt": "str" },
|
||||||
|
{ "p": "payload", "v": "virtualControl", "vt": "str" }
|
||||||
|
],
|
||||||
|
"topic": "setMode",
|
||||||
|
"repeat": "",
|
||||||
|
"crontab": "",
|
||||||
|
"once": false,
|
||||||
|
"onceDelay": "",
|
||||||
|
"x": 200,
|
||||||
|
"y": 440,
|
||||||
|
"wires": [["rm_int_pump1"]]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "rm_int_inject_start_p1",
|
||||||
|
"type": "inject",
|
||||||
|
"z": "rm_int_tab",
|
||||||
|
"name": "P1: Startup",
|
||||||
|
"props": [
|
||||||
|
{ "p": "topic", "vt": "str" },
|
||||||
|
{ "p": "payload", "v": "{\"source\":\"GUI\",\"action\":\"execSequence\",\"parameter\":\"startup\"}", "vt": "json" }
|
||||||
|
],
|
||||||
|
"topic": "execSequence",
|
||||||
|
"repeat": "",
|
||||||
|
"crontab": "",
|
||||||
|
"once": false,
|
||||||
|
"onceDelay": "",
|
||||||
|
"x": 180,
|
||||||
|
"y": 480,
|
||||||
|
"wires": [["rm_int_pump1"]]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "rm_int_inject_setpoint_p1",
|
||||||
|
"type": "inject",
|
||||||
|
"z": "rm_int_tab",
|
||||||
|
"name": "P1: Set 75%",
|
||||||
|
"props": [
|
||||||
|
{ "p": "topic", "vt": "str" },
|
||||||
|
{ "p": "payload", "v": "{\"source\":\"GUI\",\"action\":\"execMovement\",\"setpoint\":75}", "vt": "json" }
|
||||||
|
],
|
||||||
|
"topic": "execMovement",
|
||||||
|
"repeat": "",
|
||||||
|
"crontab": "",
|
||||||
|
"once": false,
|
||||||
|
"onceDelay": "",
|
||||||
|
"x": 180,
|
||||||
|
"y": 520,
|
||||||
|
"wires": [["rm_int_pump1"]]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "rm_int_inject_mode_p2",
|
||||||
|
"type": "inject",
|
||||||
|
"z": "rm_int_tab",
|
||||||
|
"name": "P2: virtualControl",
|
||||||
|
"props": [
|
||||||
|
{ "p": "topic", "vt": "str" },
|
||||||
|
{ "p": "payload", "v": "virtualControl", "vt": "str" }
|
||||||
|
],
|
||||||
|
"topic": "setMode",
|
||||||
|
"repeat": "",
|
||||||
|
"crontab": "",
|
||||||
|
"once": false,
|
||||||
|
"onceDelay": "",
|
||||||
|
"x": 200,
|
||||||
|
"y": 580,
|
||||||
|
"wires": [["rm_int_pump2"]]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "rm_int_inject_start_p2",
|
||||||
|
"type": "inject",
|
||||||
|
"z": "rm_int_tab",
|
||||||
|
"name": "P2: Startup",
|
||||||
|
"props": [
|
||||||
|
{ "p": "topic", "vt": "str" },
|
||||||
|
{ "p": "payload", "v": "{\"source\":\"GUI\",\"action\":\"execSequence\",\"parameter\":\"startup\"}", "vt": "json" }
|
||||||
|
],
|
||||||
|
"topic": "execSequence",
|
||||||
|
"repeat": "",
|
||||||
|
"crontab": "",
|
||||||
|
"once": false,
|
||||||
|
"onceDelay": "",
|
||||||
|
"x": 180,
|
||||||
|
"y": 620,
|
||||||
|
"wires": [["rm_int_pump2"]]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "rm_int_inject_setpoint_p2",
|
||||||
|
"type": "inject",
|
||||||
|
"z": "rm_int_tab",
|
||||||
|
"name": "P2: Set 50%",
|
||||||
|
"props": [
|
||||||
|
{ "p": "topic", "vt": "str" },
|
||||||
|
{ "p": "payload", "v": "{\"source\":\"GUI\",\"action\":\"execMovement\",\"setpoint\":50}", "vt": "json" }
|
||||||
|
],
|
||||||
|
"topic": "execMovement",
|
||||||
|
"repeat": "",
|
||||||
|
"crontab": "",
|
||||||
|
"once": false,
|
||||||
|
"onceDelay": "",
|
||||||
|
"x": 180,
|
||||||
|
"y": 660,
|
||||||
|
"wires": [["rm_int_pump2"]]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "rm_int_inject_pressure",
|
||||||
|
"type": "inject",
|
||||||
|
"z": "rm_int_tab",
|
||||||
|
"name": "P1: Sim downstream 900 mbar",
|
||||||
|
"props": [
|
||||||
|
{ "p": "topic", "vt": "str" },
|
||||||
|
{ "p": "payload", "v": "{\"type\":\"pressure\",\"position\":\"downstream\",\"value\":900,\"unit\":\"mbar\"}", "vt": "json" }
|
||||||
|
],
|
||||||
|
"topic": "simulateMeasurement",
|
||||||
|
"repeat": "",
|
||||||
|
"crontab": "",
|
||||||
|
"once": false,
|
||||||
|
"onceDelay": "",
|
||||||
|
"x": 240,
|
||||||
|
"y": 740,
|
||||||
|
"wires": [["rm_int_pump1"]]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "rm_int_inject_shutdown_p1",
|
||||||
|
"type": "inject",
|
||||||
|
"z": "rm_int_tab",
|
||||||
|
"name": "P1: Shutdown",
|
||||||
|
"props": [
|
||||||
|
{ "p": "topic", "vt": "str" },
|
||||||
|
{ "p": "payload", "v": "{\"source\":\"GUI\",\"action\":\"execSequence\",\"parameter\":\"shutdown\"}", "vt": "json" }
|
||||||
|
],
|
||||||
|
"topic": "execSequence",
|
||||||
|
"repeat": "",
|
||||||
|
"crontab": "",
|
||||||
|
"once": false,
|
||||||
|
"onceDelay": "",
|
||||||
|
"x": 190,
|
||||||
|
"y": 780,
|
||||||
|
"wires": [["rm_int_pump1"]]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "rm_int_inject_shutdown_p2",
|
||||||
|
"type": "inject",
|
||||||
|
"z": "rm_int_tab",
|
||||||
|
"name": "P2: Shutdown",
|
||||||
|
"props": [
|
||||||
|
{ "p": "topic", "vt": "str" },
|
||||||
|
{ "p": "payload", "v": "{\"source\":\"GUI\",\"action\":\"execSequence\",\"parameter\":\"shutdown\"}", "vt": "json" }
|
||||||
|
],
|
||||||
|
"topic": "execSequence",
|
||||||
|
"repeat": "",
|
||||||
|
"crontab": "",
|
||||||
|
"once": false,
|
||||||
|
"onceDelay": "",
|
||||||
|
"x": 190,
|
||||||
|
"y": 820,
|
||||||
|
"wires": [["rm_int_pump2"]]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "rm_int_debug_mgc_port0",
|
||||||
|
"type": "debug",
|
||||||
|
"z": "rm_int_tab",
|
||||||
|
"name": "MGC Port 0: Group State",
|
||||||
|
"active": true,
|
||||||
|
"tosidebar": true,
|
||||||
|
"console": false,
|
||||||
|
"tostatus": true,
|
||||||
|
"complete": "payload",
|
||||||
|
"targetType": "msg",
|
||||||
|
"statusVal": "payload",
|
||||||
|
"statusType": "auto",
|
||||||
|
"x": 830,
|
||||||
|
"y": 260,
|
||||||
|
"wires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "rm_int_debug_mgc_port1",
|
||||||
|
"type": "debug",
|
||||||
|
"z": "rm_int_tab",
|
||||||
|
"name": "MGC Port 1: InfluxDB",
|
||||||
|
"active": false,
|
||||||
|
"tosidebar": true,
|
||||||
|
"console": false,
|
||||||
|
"tostatus": false,
|
||||||
|
"complete": "true",
|
||||||
|
"targetType": "full",
|
||||||
|
"x": 820,
|
||||||
|
"y": 300,
|
||||||
|
"wires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "rm_int_debug_mgc_port2",
|
||||||
|
"type": "debug",
|
||||||
|
"z": "rm_int_tab",
|
||||||
|
"name": "MGC Port 2: Parent",
|
||||||
|
"active": true,
|
||||||
|
"tosidebar": true,
|
||||||
|
"console": false,
|
||||||
|
"tostatus": false,
|
||||||
|
"complete": "true",
|
||||||
|
"targetType": "full",
|
||||||
|
"x": 810,
|
||||||
|
"y": 340,
|
||||||
|
"wires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "rm_int_debug_p1_port0",
|
||||||
|
"type": "debug",
|
||||||
|
"z": "rm_int_tab",
|
||||||
|
"name": "P1 Port 0: Process",
|
||||||
|
"active": false,
|
||||||
|
"tosidebar": true,
|
||||||
|
"console": false,
|
||||||
|
"tostatus": true,
|
||||||
|
"complete": "payload",
|
||||||
|
"targetType": "msg",
|
||||||
|
"statusVal": "payload.state",
|
||||||
|
"statusType": "auto",
|
||||||
|
"x": 810,
|
||||||
|
"y": 460,
|
||||||
|
"wires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "rm_int_debug_p2_port0",
|
||||||
|
"type": "debug",
|
||||||
|
"z": "rm_int_tab",
|
||||||
|
"name": "P2 Port 0: Process",
|
||||||
|
"active": false,
|
||||||
|
"tosidebar": true,
|
||||||
|
"console": false,
|
||||||
|
"tostatus": true,
|
||||||
|
"complete": "payload",
|
||||||
|
"targetType": "msg",
|
||||||
|
"statusVal": "payload.state",
|
||||||
|
"statusType": "auto",
|
||||||
|
"x": 810,
|
||||||
|
"y": 600,
|
||||||
|
"wires": []
|
||||||
|
}
|
||||||
|
]
|
||||||
1026
examples/03 - Dashboard Visualization.json
Normal file
1026
examples/03 - Dashboard Visualization.json
Normal file
File diff suppressed because it is too large
Load Diff
53
examples/README.md
Normal file
53
examples/README.md
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
# RotatingMachine Example Flows
|
||||||
|
|
||||||
|
These flows are import-ready Node-RED examples for the `rotatingMachine` node.
|
||||||
|
In Node-RED: **Import > Examples > EVOLV** to find them.
|
||||||
|
|
||||||
|
## Example Flows
|
||||||
|
|
||||||
|
### 01 - Basic Manual Control
|
||||||
|
**Dependencies:** EVOLV only (no dashboard)
|
||||||
|
|
||||||
|
Inject-based flow demonstrating all core functionality:
|
||||||
|
- Mode switching (auto / virtualControl / fysicalControl)
|
||||||
|
- Startup/shutdown/emergency sequences
|
||||||
|
- Speed setpoint control (30%, 60%, 100%)
|
||||||
|
- Pressure simulation (upstream + downstream)
|
||||||
|
- Maintenance mode enter/leave
|
||||||
|
- Debug outputs on all 3 ports
|
||||||
|
|
||||||
|
### 02 - Integration with Machine Group
|
||||||
|
**Dependencies:** EVOLV only (no dashboard)
|
||||||
|
|
||||||
|
Parent-child relationship demo:
|
||||||
|
- machineGroupControl parent with 2x rotatingMachine children
|
||||||
|
- Auto-registration via Port 2 on deploy
|
||||||
|
- Independent pump control with group-level aggregation
|
||||||
|
- Pressure simulation on individual pumps
|
||||||
|
|
||||||
|
### 03 - Dashboard Visualization
|
||||||
|
**Dependencies:** EVOLV + @flowfuse/node-red-dashboard
|
||||||
|
|
||||||
|
Interactive FlowFuse dashboard with:
|
||||||
|
- Mode dropdown, startup/shutdown/emergency buttons
|
||||||
|
- Speed setpoint input, pressure simulation inputs
|
||||||
|
- Real-time charts: flow, power, ctrl%, NCog, state code, pressure
|
||||||
|
|
||||||
|
## Legacy Files
|
||||||
|
|
||||||
|
The following files are from the original flow set and will be removed in a future release:
|
||||||
|
- `basic.flow.json` → replaced by `01 - Basic Manual Control.json`
|
||||||
|
- `integration.flow.json` → replaced by `02 - Integration with Machine Group.json`
|
||||||
|
- `edge.flow.json` → edge-case testing (inject-based)
|
||||||
|
|
||||||
|
## Import
|
||||||
|
|
||||||
|
1. In Node-RED, use **Import > Examples > EVOLV** (auto-discovered)
|
||||||
|
2. Or manually: **Import > Clipboard** and paste the `.json` file contents
|
||||||
|
3. Deploy
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Tier 1 and 2 examples have zero dashboard dependencies — they work on any Node-RED install with EVOLV
|
||||||
|
- Tier 3 requires `@flowfuse/node-red-dashboard` (included in EVOLV's package.json dependencies)
|
||||||
|
- All examples use `enableLog: true` so you can observe behavior in the Node-RED debug panel
|
||||||
1016
examples/basic.flow.json
Normal file
1016
examples/basic.flow.json
Normal file
File diff suppressed because it is too large
Load Diff
327
examples/edge.flow.json
Normal file
327
examples/edge.flow.json
Normal file
@@ -0,0 +1,327 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "91a88f212fb34de8",
|
||||||
|
"type": "tab",
|
||||||
|
"label": "RotatingMachine Edge Cases",
|
||||||
|
"disabled": false,
|
||||||
|
"info": "Manual edge-case driving for rotatingMachine"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ui_base_rm_edge",
|
||||||
|
"type": "ui-base",
|
||||||
|
"name": "EVOLV Demo",
|
||||||
|
"path": "/dashboard",
|
||||||
|
"includeClientData": true,
|
||||||
|
"acceptsClientConfig": [
|
||||||
|
"ui-notification",
|
||||||
|
"ui-control"
|
||||||
|
],
|
||||||
|
"showPathInSidebar": false,
|
||||||
|
"headerContent": "page",
|
||||||
|
"navigationStyle": "default",
|
||||||
|
"titleBarStyle": "default"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ui_theme_rm_edge",
|
||||||
|
"type": "ui-theme",
|
||||||
|
"name": "EVOLV Edge Theme",
|
||||||
|
"colors": {
|
||||||
|
"surface": "#ffffff",
|
||||||
|
"primary": "#0094ce",
|
||||||
|
"bgPage": "#eeeeee",
|
||||||
|
"groupBg": "#ffffff",
|
||||||
|
"groupOutline": "#cccccc"
|
||||||
|
},
|
||||||
|
"sizes": {
|
||||||
|
"density": "default",
|
||||||
|
"pagePadding": "12px",
|
||||||
|
"groupGap": "12px",
|
||||||
|
"groupBorderRadius": "4px",
|
||||||
|
"widgetGap": "12px"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ui_page_rm_edge",
|
||||||
|
"type": "ui-page",
|
||||||
|
"name": "RotatingMachine Edge",
|
||||||
|
"ui": "ui_base_rm_edge",
|
||||||
|
"path": "/rotating-machine-edge",
|
||||||
|
"icon": "report_problem",
|
||||||
|
"layout": "grid",
|
||||||
|
"theme": "ui_theme_rm_edge",
|
||||||
|
"breakpoints": [
|
||||||
|
{
|
||||||
|
"name": "Default",
|
||||||
|
"px": "0",
|
||||||
|
"cols": "12"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"order": 3,
|
||||||
|
"className": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ui_group_rm_edge_inputs",
|
||||||
|
"type": "ui-group",
|
||||||
|
"name": "Edge Input Generators",
|
||||||
|
"page": "ui_page_rm_edge",
|
||||||
|
"width": "6",
|
||||||
|
"height": "1",
|
||||||
|
"order": 1,
|
||||||
|
"showTitle": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ui_group_rm_edge_obs",
|
||||||
|
"type": "ui-group",
|
||||||
|
"name": "Observed Responses",
|
||||||
|
"page": "ui_page_rm_edge",
|
||||||
|
"width": "6",
|
||||||
|
"height": "8",
|
||||||
|
"order": 2,
|
||||||
|
"showTitle": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "rm_node_edge",
|
||||||
|
"type": "rotatingMachine",
|
||||||
|
"z": "91a88f212fb34de8",
|
||||||
|
"name": "RM Edge",
|
||||||
|
"speed": "1",
|
||||||
|
"startup": "3",
|
||||||
|
"warmup": "3",
|
||||||
|
"shutdown": "3",
|
||||||
|
"cooldown": "3",
|
||||||
|
"movementMode": "staticspeed",
|
||||||
|
"machineCurve": "",
|
||||||
|
"uuid": "",
|
||||||
|
"supplier": "hidrostal",
|
||||||
|
"category": "machine",
|
||||||
|
"assetType": "pump-centrifugal",
|
||||||
|
"model": "hidrostal-H05K-S03R",
|
||||||
|
"unit": "m3/h",
|
||||||
|
"enableLog": false,
|
||||||
|
"logLevel": "error",
|
||||||
|
"positionVsParent": "atEquipment",
|
||||||
|
"x": 930,
|
||||||
|
"y": 300,
|
||||||
|
"wires": [
|
||||||
|
[
|
||||||
|
"rm_edge_parse",
|
||||||
|
"rm_edge_process_debug"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"rm_edge_debug_influx"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"rm_edge_debug_parent"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"hasDistance": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "rm_edge_invalid_mode_btn",
|
||||||
|
"type": "ui-button",
|
||||||
|
"z": "91a88f212fb34de8",
|
||||||
|
"group": "ui_group_rm_edge_inputs",
|
||||||
|
"name": "Invalid Mode",
|
||||||
|
"label": "Send invalid mode",
|
||||||
|
"order": 1,
|
||||||
|
"width": "6",
|
||||||
|
"height": "1",
|
||||||
|
"payload": "invalidMode",
|
||||||
|
"payloadType": "str",
|
||||||
|
"topic": "setMode",
|
||||||
|
"x": 190,
|
||||||
|
"y": 120,
|
||||||
|
"wires": [
|
||||||
|
[
|
||||||
|
"rm_node_edge"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "rm_edge_neg_setpoint",
|
||||||
|
"type": "ui-number-input",
|
||||||
|
"z": "91a88f212fb34de8",
|
||||||
|
"group": "ui_group_rm_edge_inputs",
|
||||||
|
"name": "Negative Setpoint",
|
||||||
|
"label": "Negative setpoint",
|
||||||
|
"order": 2,
|
||||||
|
"width": "6",
|
||||||
|
"height": "1",
|
||||||
|
"passthru": true,
|
||||||
|
"min": -100,
|
||||||
|
"max": 0,
|
||||||
|
"step": 1,
|
||||||
|
"x": 190,
|
||||||
|
"y": 170,
|
||||||
|
"wires": [
|
||||||
|
[
|
||||||
|
"rm_edge_exec_movement"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "rm_edge_exec_movement",
|
||||||
|
"type": "function",
|
||||||
|
"z": "91a88f212fb34de8",
|
||||||
|
"name": "Build execMovement",
|
||||||
|
"func": "msg.topic = 'execMovement';\nmsg.payload = {source:'GUI', action:'execMovement', setpoint:Number(msg.payload)};\nreturn msg;",
|
||||||
|
"outputs": 1,
|
||||||
|
"x": 450,
|
||||||
|
"y": 170,
|
||||||
|
"wires": [
|
||||||
|
[
|
||||||
|
"rm_node_edge"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "rm_edge_bad_source_btn",
|
||||||
|
"type": "ui-button",
|
||||||
|
"z": "91a88f212fb34de8",
|
||||||
|
"group": "ui_group_rm_edge_inputs",
|
||||||
|
"name": "Bad Source",
|
||||||
|
"label": "Disallowed source action",
|
||||||
|
"order": 3,
|
||||||
|
"width": "6",
|
||||||
|
"height": "1",
|
||||||
|
"payload": "{}",
|
||||||
|
"payloadType": "str",
|
||||||
|
"x": 210,
|
||||||
|
"y": 220,
|
||||||
|
"wires": [
|
||||||
|
[
|
||||||
|
"rm_edge_bad_source_builder"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "rm_edge_bad_source_builder",
|
||||||
|
"type": "function",
|
||||||
|
"z": "91a88f212fb34de8",
|
||||||
|
"name": "Build blocked execSequence",
|
||||||
|
"func": "msg.topic='execSequence';\nmsg.payload={source:'bad-source', action:'execSequence', parameter:'startup'};\nreturn msg;",
|
||||||
|
"outputs": 1,
|
||||||
|
"x": 500,
|
||||||
|
"y": 220,
|
||||||
|
"wires": [
|
||||||
|
[
|
||||||
|
"rm_node_edge"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "rm_edge_bad_sim_type",
|
||||||
|
"type": "ui-button",
|
||||||
|
"z": "91a88f212fb34de8",
|
||||||
|
"group": "ui_group_rm_edge_inputs",
|
||||||
|
"name": "Unsupported Sim Type",
|
||||||
|
"label": "simulateMeasurement (bad type)",
|
||||||
|
"order": 4,
|
||||||
|
"width": "6",
|
||||||
|
"height": "1",
|
||||||
|
"payload": "{\"type\":\"unknown\",\"position\":\"downstream\",\"value\":123,\"unit\":\"mbar\"}",
|
||||||
|
"payloadType": "json",
|
||||||
|
"topic": "simulateMeasurement",
|
||||||
|
"x": 220,
|
||||||
|
"y": 270,
|
||||||
|
"wires": [
|
||||||
|
[
|
||||||
|
"rm_node_edge"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "rm_edge_parse",
|
||||||
|
"type": "function",
|
||||||
|
"z": "91a88f212fb34de8",
|
||||||
|
"name": "Summarize process output",
|
||||||
|
"func": "const incoming = (msg && msg.payload && typeof msg.payload === 'object') ? msg.payload : {};\nconst merged = { ...(context.get('lastPayload') || {}), ...incoming };\ncontext.set('lastPayload', merged);\n\nconst state = merged.state == null ? 'n/a' : String(merged.state);\nconst mode = merged.mode == null ? 'n/a' : String(merged.mode);\nconst ctrl = Number(merged.ctrl);\nconst stateCodeMap = { off: 0, idle: 1, starting: 2, warmingup: 3, operational: 4, accelerating: 5, decelerating: 6, stopping: 7, coolingdown: 8, maintenance: 9 };\nconst stateCode = stateCodeMap[state] ?? -1;\n\nreturn [\n { payload: `state=${state}, mode=${mode}, ctrl=${Number.isFinite(ctrl) ? ctrl.toFixed(2) : 'n/a'}%` },\n { topic: 'stateCode', payload: stateCode }\n];",
|
||||||
|
"outputs": 2,
|
||||||
|
"x": 1230,
|
||||||
|
"y": 300,
|
||||||
|
"wires": [
|
||||||
|
[
|
||||||
|
"rm_edge_state_text"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"rm_edge_state_chart"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "rm_edge_state_text",
|
||||||
|
"type": "ui-text",
|
||||||
|
"z": "91a88f212fb34de8",
|
||||||
|
"group": "ui_group_rm_edge_obs",
|
||||||
|
"name": "State Text",
|
||||||
|
"label": "Machine summary",
|
||||||
|
"order": 2,
|
||||||
|
"format": "{{msg.payload}}",
|
||||||
|
"layout": "row-spread",
|
||||||
|
"x": 1450,
|
||||||
|
"y": 300,
|
||||||
|
"wires": [],
|
||||||
|
"width": 6,
|
||||||
|
"height": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "rm_edge_process_debug",
|
||||||
|
"type": "debug",
|
||||||
|
"z": "91a88f212fb34de8",
|
||||||
|
"name": "Process Output",
|
||||||
|
"active": true,
|
||||||
|
"tosidebar": true,
|
||||||
|
"complete": "true",
|
||||||
|
"targetType": "full",
|
||||||
|
"x": 1220,
|
||||||
|
"y": 340,
|
||||||
|
"wires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "rm_edge_debug_influx",
|
||||||
|
"type": "debug",
|
||||||
|
"z": "91a88f212fb34de8",
|
||||||
|
"name": "Influx Output",
|
||||||
|
"active": true,
|
||||||
|
"tosidebar": true,
|
||||||
|
"complete": "true",
|
||||||
|
"targetType": "full",
|
||||||
|
"x": 1220,
|
||||||
|
"y": 380,
|
||||||
|
"wires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "rm_edge_debug_parent",
|
||||||
|
"type": "debug",
|
||||||
|
"z": "91a88f212fb34de8",
|
||||||
|
"name": "Parent Output",
|
||||||
|
"active": true,
|
||||||
|
"tosidebar": true,
|
||||||
|
"complete": "true",
|
||||||
|
"targetType": "full",
|
||||||
|
"x": 1220,
|
||||||
|
"y": 420,
|
||||||
|
"wires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "rm_edge_state_chart",
|
||||||
|
"type": "ui-chart",
|
||||||
|
"z": "91a88f212fb34de8",
|
||||||
|
"group": "ui_group_rm_edge_obs",
|
||||||
|
"name": "State Code",
|
||||||
|
"label": "State Code",
|
||||||
|
"order": 1,
|
||||||
|
"width": 6,
|
||||||
|
"height": 4,
|
||||||
|
"chartType": "line",
|
||||||
|
"category": "topic",
|
||||||
|
"categoryType": "msg",
|
||||||
|
"xAxisType": "time",
|
||||||
|
"yAxisLabel": "state",
|
||||||
|
"removeOlder": "1",
|
||||||
|
"removeOlderUnit": "3600",
|
||||||
|
"x": 1230,
|
||||||
|
"y": 300,
|
||||||
|
"wires": []
|
||||||
|
}
|
||||||
|
]
|
||||||
759
examples/integration.flow.json
Normal file
759
examples/integration.flow.json
Normal file
@@ -0,0 +1,759 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "12f41a7b538c40db",
|
||||||
|
"type": "tab",
|
||||||
|
"label": "RotatingMachine Integration",
|
||||||
|
"disabled": false,
|
||||||
|
"info": "Manual integration-style scenario builder for rotatingMachine"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ui_base_rm_int",
|
||||||
|
"type": "ui-base",
|
||||||
|
"name": "EVOLV Demo",
|
||||||
|
"path": "/dashboard",
|
||||||
|
"includeClientData": true,
|
||||||
|
"acceptsClientConfig": [
|
||||||
|
"ui-notification",
|
||||||
|
"ui-control"
|
||||||
|
],
|
||||||
|
"showPathInSidebar": false,
|
||||||
|
"headerContent": "page",
|
||||||
|
"navigationStyle": "default",
|
||||||
|
"titleBarStyle": "default"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ui_theme_rm_int",
|
||||||
|
"type": "ui-theme",
|
||||||
|
"name": "EVOLV Integration Theme",
|
||||||
|
"colors": {
|
||||||
|
"surface": "#ffffff",
|
||||||
|
"primary": "#0094ce",
|
||||||
|
"bgPage": "#eeeeee",
|
||||||
|
"groupBg": "#ffffff",
|
||||||
|
"groupOutline": "#cccccc"
|
||||||
|
},
|
||||||
|
"sizes": {
|
||||||
|
"density": "default",
|
||||||
|
"pagePadding": "14px",
|
||||||
|
"groupGap": "14px",
|
||||||
|
"groupBorderRadius": "6px",
|
||||||
|
"widgetGap": "12px"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ui_page_rm_int",
|
||||||
|
"type": "ui-page",
|
||||||
|
"name": "RotatingMachine Integration",
|
||||||
|
"ui": "ui_base_rm_int",
|
||||||
|
"path": "/rotating-machine-integration",
|
||||||
|
"icon": "lan",
|
||||||
|
"layout": "grid",
|
||||||
|
"theme": "ui_theme_rm_int",
|
||||||
|
"breakpoints": [
|
||||||
|
{
|
||||||
|
"name": "Default",
|
||||||
|
"px": "0",
|
||||||
|
"cols": "12"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"order": 2,
|
||||||
|
"className": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ui_group_rm_int_ctrl",
|
||||||
|
"type": "ui-group",
|
||||||
|
"name": "Control Sequences",
|
||||||
|
"page": "ui_page_rm_int",
|
||||||
|
"width": "6",
|
||||||
|
"height": "1",
|
||||||
|
"order": 1,
|
||||||
|
"showTitle": true,
|
||||||
|
"className": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ui_group_rm_int_sim",
|
||||||
|
"type": "ui-group",
|
||||||
|
"name": "Process Simulation",
|
||||||
|
"page": "ui_page_rm_int",
|
||||||
|
"width": "6",
|
||||||
|
"height": "1",
|
||||||
|
"order": 2,
|
||||||
|
"showTitle": true,
|
||||||
|
"className": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ui_group_rm_int_vis",
|
||||||
|
"type": "ui-group",
|
||||||
|
"name": "Observed Behaviour",
|
||||||
|
"page": "ui_page_rm_int",
|
||||||
|
"width": "12",
|
||||||
|
"height": "24",
|
||||||
|
"order": 3,
|
||||||
|
"showTitle": true,
|
||||||
|
"className": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "rm_node_int",
|
||||||
|
"type": "rotatingMachine",
|
||||||
|
"z": "12f41a7b538c40db",
|
||||||
|
"name": "RM Integration",
|
||||||
|
"speed": "1",
|
||||||
|
"startup": "3",
|
||||||
|
"warmup": "3",
|
||||||
|
"shutdown": "3",
|
||||||
|
"cooldown": "3",
|
||||||
|
"movementMode": "staticspeed",
|
||||||
|
"machineCurve": "",
|
||||||
|
"uuid": "",
|
||||||
|
"supplier": "hidrostal",
|
||||||
|
"category": "machine",
|
||||||
|
"assetType": "pump-centrifugal",
|
||||||
|
"model": "hidrostal-H05K-S03R",
|
||||||
|
"unit": "m3/h",
|
||||||
|
"enableLog": false,
|
||||||
|
"logLevel": "error",
|
||||||
|
"positionVsParent": "atEquipment",
|
||||||
|
"x": 1040,
|
||||||
|
"y": 360,
|
||||||
|
"wires": [
|
||||||
|
[
|
||||||
|
"rm_int_parse"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"rm_int_debug_influx"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"rm_int_debug_parent"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"hasDistance": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "rm_int_startup",
|
||||||
|
"type": "ui-button",
|
||||||
|
"z": "12f41a7b538c40db",
|
||||||
|
"group": "ui_group_rm_int_ctrl",
|
||||||
|
"name": "Startup",
|
||||||
|
"label": "Startup",
|
||||||
|
"order": 1,
|
||||||
|
"width": "3",
|
||||||
|
"height": "1",
|
||||||
|
"icon": "play_arrow",
|
||||||
|
"payload": "startup",
|
||||||
|
"payloadType": "str",
|
||||||
|
"x": 190,
|
||||||
|
"y": 120,
|
||||||
|
"wires": [
|
||||||
|
[
|
||||||
|
"rm_int_exec_seq"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "rm_int_shutdown",
|
||||||
|
"type": "ui-button",
|
||||||
|
"z": "12f41a7b538c40db",
|
||||||
|
"group": "ui_group_rm_int_ctrl",
|
||||||
|
"name": "Shutdown",
|
||||||
|
"label": "Shutdown",
|
||||||
|
"order": 2,
|
||||||
|
"width": "3",
|
||||||
|
"height": "1",
|
||||||
|
"icon": "stop",
|
||||||
|
"payload": "shutdown",
|
||||||
|
"payloadType": "str",
|
||||||
|
"x": 190,
|
||||||
|
"y": 160,
|
||||||
|
"wires": [
|
||||||
|
[
|
||||||
|
"rm_int_exec_seq"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "rm_int_maint",
|
||||||
|
"type": "ui-button",
|
||||||
|
"z": "12f41a7b538c40db",
|
||||||
|
"group": "ui_group_rm_int_ctrl",
|
||||||
|
"name": "Maintenance",
|
||||||
|
"label": "Enter Maintenance",
|
||||||
|
"order": 3,
|
||||||
|
"width": "6",
|
||||||
|
"height": "1",
|
||||||
|
"icon": "build",
|
||||||
|
"payload": "entermaintenance",
|
||||||
|
"payloadType": "str",
|
||||||
|
"x": 220,
|
||||||
|
"y": 200,
|
||||||
|
"wires": [
|
||||||
|
[
|
||||||
|
"rm_int_exec_seq"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "rm_int_exec_seq",
|
||||||
|
"type": "function",
|
||||||
|
"z": "12f41a7b538c40db",
|
||||||
|
"name": "Build execSequence",
|
||||||
|
"func": "msg.topic = 'execSequence';\nmsg.payload = {source:'GUI', action:'execSequence', parameter: msg.payload};\nreturn msg;",
|
||||||
|
"outputs": 1,
|
||||||
|
"x": 450,
|
||||||
|
"y": 160,
|
||||||
|
"wires": [
|
||||||
|
[
|
||||||
|
"rm_node_int"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "rm_int_setpoint",
|
||||||
|
"type": "ui-number-input",
|
||||||
|
"z": "12f41a7b538c40db",
|
||||||
|
"group": "ui_group_rm_int_ctrl",
|
||||||
|
"name": "Setpoint",
|
||||||
|
"label": "Setpoint %",
|
||||||
|
"order": 4,
|
||||||
|
"width": "6",
|
||||||
|
"height": "1",
|
||||||
|
"passthru": true,
|
||||||
|
"min": 0,
|
||||||
|
"max": 100,
|
||||||
|
"step": 1,
|
||||||
|
"x": 190,
|
||||||
|
"y": 260,
|
||||||
|
"wires": [
|
||||||
|
[
|
||||||
|
"rm_int_exec_movement",
|
||||||
|
"rm_int_ctrl_setpoint_for_chart"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "rm_int_exec_movement",
|
||||||
|
"type": "function",
|
||||||
|
"z": "12f41a7b538c40db",
|
||||||
|
"name": "Build execMovement",
|
||||||
|
"func": "msg.topic='execMovement';\nmsg.payload={source:'GUI', action:'execMovement', setpoint:Number(msg.payload)};\nreturn msg;",
|
||||||
|
"outputs": 1,
|
||||||
|
"x": 460,
|
||||||
|
"y": 260,
|
||||||
|
"wires": [
|
||||||
|
[
|
||||||
|
"rm_node_int"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "rm_int_flow_target",
|
||||||
|
"type": "ui-number-input",
|
||||||
|
"z": "12f41a7b538c40db",
|
||||||
|
"group": "ui_group_rm_int_sim",
|
||||||
|
"name": "Flow target",
|
||||||
|
"label": "Flow target (m3/h)",
|
||||||
|
"order": 1,
|
||||||
|
"width": "6",
|
||||||
|
"height": "1",
|
||||||
|
"passthru": true,
|
||||||
|
"min": 0,
|
||||||
|
"max": 1000,
|
||||||
|
"step": 5,
|
||||||
|
"x": 200,
|
||||||
|
"y": 340,
|
||||||
|
"wires": [
|
||||||
|
[
|
||||||
|
"rm_int_flow_move",
|
||||||
|
"rm_int_flow_setpoint_for_chart"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "rm_int_flow_move",
|
||||||
|
"type": "function",
|
||||||
|
"z": "12f41a7b538c40db",
|
||||||
|
"name": "Build flowMovement",
|
||||||
|
"func": "msg.topic='flowMovement';\nmsg.payload={source:'GUI', action:'flowMovement', setpoint:Number(msg.payload)};\nreturn msg;",
|
||||||
|
"outputs": 1,
|
||||||
|
"x": 450,
|
||||||
|
"y": 340,
|
||||||
|
"wires": [
|
||||||
|
[
|
||||||
|
"rm_node_int"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "rm_int_pressure_up",
|
||||||
|
"type": "ui-number-input",
|
||||||
|
"z": "12f41a7b538c40db",
|
||||||
|
"group": "ui_group_rm_int_sim",
|
||||||
|
"name": "Upstream Pressure",
|
||||||
|
"label": "Upstream pressure (mbar)",
|
||||||
|
"order": 2,
|
||||||
|
"width": "6",
|
||||||
|
"height": "1",
|
||||||
|
"passthru": true,
|
||||||
|
"min": 0,
|
||||||
|
"max": 3000,
|
||||||
|
"step": 10,
|
||||||
|
"x": 210,
|
||||||
|
"y": 390,
|
||||||
|
"wires": [
|
||||||
|
[
|
||||||
|
"rm_int_sim_up"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "rm_int_pressure_down",
|
||||||
|
"type": "ui-number-input",
|
||||||
|
"z": "12f41a7b538c40db",
|
||||||
|
"group": "ui_group_rm_int_sim",
|
||||||
|
"name": "Downstream Pressure",
|
||||||
|
"label": "Downstream pressure (mbar)",
|
||||||
|
"order": 3,
|
||||||
|
"width": "6",
|
||||||
|
"height": "1",
|
||||||
|
"passthru": true,
|
||||||
|
"min": 0,
|
||||||
|
"max": 3000,
|
||||||
|
"step": 10,
|
||||||
|
"x": 220,
|
||||||
|
"y": 430,
|
||||||
|
"wires": [
|
||||||
|
[
|
||||||
|
"rm_int_sim_down"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "rm_int_sim_up",
|
||||||
|
"type": "function",
|
||||||
|
"z": "12f41a7b538c40db",
|
||||||
|
"name": "simulate upstream pressure",
|
||||||
|
"func": "msg.topic='simulateMeasurement';\nmsg.payload={type:'pressure', position:'upstream', value:Number(msg.payload), unit:'mbar'};\nreturn msg;",
|
||||||
|
"outputs": 1,
|
||||||
|
"x": 500,
|
||||||
|
"y": 390,
|
||||||
|
"wires": [
|
||||||
|
[
|
||||||
|
"rm_node_int"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "rm_int_sim_down",
|
||||||
|
"type": "function",
|
||||||
|
"z": "12f41a7b538c40db",
|
||||||
|
"name": "simulate downstream pressure",
|
||||||
|
"func": "msg.topic='simulateMeasurement';\nmsg.payload={type:'pressure', position:'downstream', value:Number(msg.payload), unit:'mbar'};\nreturn msg;",
|
||||||
|
"outputs": 1,
|
||||||
|
"x": 510,
|
||||||
|
"y": 430,
|
||||||
|
"wires": [
|
||||||
|
[
|
||||||
|
"rm_node_int"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "rm_int_parse",
|
||||||
|
"type": "function",
|
||||||
|
"z": "12f41a7b538c40db",
|
||||||
|
"name": "Parse RM output",
|
||||||
|
"func": "const incoming = (msg && msg.payload && typeof msg.payload === 'object') ? msg.payload : {};\nconst lastPayload = context.get('lastPayload') || {};\nconst merged = { ...lastPayload, ...incoming };\ncontext.set('lastPayload', merged);\n\nconst cache = context.get('metricCache') || {\n flow: 0,\n power: 0,\n ctrl: 0,\n nCog: 0,\n stateCode: 0,\n state: 'idle',\n mode: 'auto',\n runtime: 0,\n moveTimeleft: 0,\n maintenanceTime: 0,\n pressureUp: null,\n pressureDown: null,\n};\n\nconst pickNumber = (...keys) => {\n for (const key of keys) {\n const value = Number(merged[key]);\n if (Number.isFinite(value)) return value;\n }\n return null;\n};\n\nconst pickByPrefix = (...prefixes) => {\n const keys = Object.keys(merged);\n for (const prefix of prefixes) {\n const direct = Number(merged[prefix]);\n if (Number.isFinite(direct)) return direct;\n\n const dynamicKey = keys.find((k) => k === prefix || k.startsWith(prefix + '.'));\n if (!dynamicKey) continue;\n\n const value = Number(merged[dynamicKey]);\n if (Number.isFinite(value)) return value;\n }\n return null;\n};\n\nconst pickString = (key, fallback = null) => {\n const value = merged[key];\n if (value === undefined || value === null || value === '') return fallback;\n return String(value);\n};\n\nconst flowValue = pickByPrefix('flow.predicted.downstream');\nconst power = pickByPrefix('power.predicted.atequipment', 'power.predicted.atEquipment');\nconst ctrl = pickNumber('ctrl') ?? pickByPrefix('ctrl.predicted.atequipment', 'ctrl.predicted.atEquipment');\nconst nCog = pickNumber('NCogPercent', 'NCog');\nconst runtime = pickNumber('runtime');\nconst moveTimeleft = pickNumber('moveTimeleft');\nconst maintenanceTime = pickNumber('maintenanceTime');\nconst pressureDownIncoming = pickByPrefix('pressure.measured.downstream');\nconst pressureUpIncoming = pickByPrefix('pressure.measured.upstream');\nconst state = pickString('state', cache.state);\nconst mode = pickString('mode', cache.mode);\n\nconst stateCodeMap = { off: 0, idle: 1, starting: 2, warmingup: 3, operational: 4, accelerating: 5, decelerating: 6, stopping: 7, coolingdown: 8, maintenance: 9 };\nconst stateCode = stateCodeMap[state] ?? cache.stateCode;\n\nif (flowValue !== null) cache.flow = flowValue;\nif (power !== null) cache.power = power;\nif (ctrl !== null) cache.ctrl = ctrl;\nif (nCog !== null) cache.nCog = nCog;\nif (runtime !== null) cache.runtime = runtime;\nif (moveTimeleft !== null) cache.moveTimeleft = moveTimeleft;\nif (maintenanceTime !== null) cache.maintenanceTime = maintenanceTime;\nif (pressureUpIncoming !== null) cache.pressureUp = pressureUpIncoming;\nif (pressureDownIncoming !== null) cache.pressureDown = pressureDownIncoming;\ncache.state = state;\ncache.mode = mode;\ncache.stateCode = stateCode;\ncontext.set('metricCache', cache);\n\nconst pressureUp = Number.isFinite(cache.pressureUp) ? cache.pressureUp : null;\nconst pressureDown = Number.isFinite(cache.pressureDown) ? cache.pressureDown : null;\nconst pressureDelta = (pressureDown !== null && pressureUp !== null) ? (pressureDown - pressureUp) : null;\n\nconst now = Date.now();\nreturn [\n { topic: 'actual_flow', payload: cache.flow, timestamp: now },\n { topic: 'predicted_power', payload: cache.power, timestamp: now },\n { topic: 'nCog', payload: cache.nCog, timestamp: now },\n { topic: 'actual_ctrl', payload: cache.ctrl, timestamp: now },\n { topic: 'stateCode', payload: cache.stateCode, timestamp: now },\n { payload: JSON.stringify({ state: cache.state, mode: cache.mode, ctrl: cache.ctrl, runtime: cache.runtime, moveTimeleft: cache.moveTimeleft, maintenanceTime: cache.maintenanceTime, pressureUp, pressureDown, pressureDelta }) }\n];",
|
||||||
|
"outputs": 6,
|
||||||
|
"x": 1260,
|
||||||
|
"y": 360,
|
||||||
|
"wires": [
|
||||||
|
[
|
||||||
|
"rm_int_chart_flow"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"rm_int_chart_power"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"rm_int_chart_nCog"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"rm_int_chart_ctrl"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"rm_int_chart_statecode"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"rm_int_state_text"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "rm_int_chart_flow",
|
||||||
|
"type": "ui-chart",
|
||||||
|
"z": "12f41a7b538c40db",
|
||||||
|
"group": "ui_group_rm_int_vis",
|
||||||
|
"name": "Flow",
|
||||||
|
"label": "Flow (m3/h)",
|
||||||
|
"order": 1,
|
||||||
|
"width": 6,
|
||||||
|
"height": 4,
|
||||||
|
"chartType": "line",
|
||||||
|
"xAxisType": "time",
|
||||||
|
"yAxisLabel": "m3/h",
|
||||||
|
"removeOlder": "1",
|
||||||
|
"removeOlderUnit": "3600",
|
||||||
|
"x": 1510,
|
||||||
|
"y": 280,
|
||||||
|
"wires": [],
|
||||||
|
"showLegend": true,
|
||||||
|
"category": "topic",
|
||||||
|
"categoryType": "msg",
|
||||||
|
"xAxisProperty": "",
|
||||||
|
"xAxisPropertyType": "timestamp",
|
||||||
|
"xAxisFormat": "",
|
||||||
|
"xAxisFormatType": "auto",
|
||||||
|
"yAxisProperty": "payload",
|
||||||
|
"yAxisPropertyType": "msg",
|
||||||
|
"xmin": "",
|
||||||
|
"xmax": "",
|
||||||
|
"ymin": "",
|
||||||
|
"ymax": "",
|
||||||
|
"bins": 10,
|
||||||
|
"action": "append",
|
||||||
|
"stackSeries": false,
|
||||||
|
"pointShape": "circle",
|
||||||
|
"pointRadius": 4,
|
||||||
|
"interpolation": "linear",
|
||||||
|
"className": "",
|
||||||
|
"colors": [
|
||||||
|
"#0095FF",
|
||||||
|
"#FF0000",
|
||||||
|
"#FF7F0E",
|
||||||
|
"#2CA02C",
|
||||||
|
"#A347E1",
|
||||||
|
"#D62728",
|
||||||
|
"#FF9896",
|
||||||
|
"#9467BD",
|
||||||
|
"#C5B0D5"
|
||||||
|
],
|
||||||
|
"textColor": [
|
||||||
|
"#666666"
|
||||||
|
],
|
||||||
|
"textColorDefault": true,
|
||||||
|
"gridColor": [
|
||||||
|
"#e5e5e5"
|
||||||
|
],
|
||||||
|
"gridColorDefault": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "rm_int_chart_power",
|
||||||
|
"type": "ui-chart",
|
||||||
|
"z": "12f41a7b538c40db",
|
||||||
|
"group": "ui_group_rm_int_vis",
|
||||||
|
"name": "Power",
|
||||||
|
"label": "Power (kW)",
|
||||||
|
"order": 2,
|
||||||
|
"width": 6,
|
||||||
|
"height": 4,
|
||||||
|
"chartType": "line",
|
||||||
|
"xAxisType": "time",
|
||||||
|
"yAxisLabel": "kW",
|
||||||
|
"removeOlder": "1",
|
||||||
|
"removeOlderUnit": "3600",
|
||||||
|
"x": 1510,
|
||||||
|
"y": 340,
|
||||||
|
"wires": [],
|
||||||
|
"showLegend": true,
|
||||||
|
"category": "topic",
|
||||||
|
"categoryType": "msg",
|
||||||
|
"xAxisProperty": "",
|
||||||
|
"xAxisPropertyType": "timestamp",
|
||||||
|
"xAxisFormat": "",
|
||||||
|
"xAxisFormatType": "auto",
|
||||||
|
"yAxisProperty": "payload",
|
||||||
|
"yAxisPropertyType": "msg",
|
||||||
|
"xmin": "",
|
||||||
|
"xmax": "",
|
||||||
|
"ymin": "",
|
||||||
|
"ymax": "",
|
||||||
|
"bins": 10,
|
||||||
|
"action": "append",
|
||||||
|
"stackSeries": false,
|
||||||
|
"pointShape": "circle",
|
||||||
|
"pointRadius": 4,
|
||||||
|
"interpolation": "linear",
|
||||||
|
"className": "",
|
||||||
|
"colors": [
|
||||||
|
"#0095FF",
|
||||||
|
"#FF0000",
|
||||||
|
"#FF7F0E",
|
||||||
|
"#2CA02C",
|
||||||
|
"#A347E1",
|
||||||
|
"#D62728",
|
||||||
|
"#FF9896",
|
||||||
|
"#9467BD",
|
||||||
|
"#C5B0D5"
|
||||||
|
],
|
||||||
|
"textColor": [
|
||||||
|
"#666666"
|
||||||
|
],
|
||||||
|
"textColorDefault": true,
|
||||||
|
"gridColor": [
|
||||||
|
"#e5e5e5"
|
||||||
|
],
|
||||||
|
"gridColorDefault": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "rm_int_chart_nCog",
|
||||||
|
"type": "ui-chart",
|
||||||
|
"z": "12f41a7b538c40db",
|
||||||
|
"group": "ui_group_rm_int_vis",
|
||||||
|
"name": "NCog",
|
||||||
|
"label": "NCog (%)",
|
||||||
|
"order": 3,
|
||||||
|
"width": 6,
|
||||||
|
"height": 4,
|
||||||
|
"chartType": "line",
|
||||||
|
"xAxisType": "time",
|
||||||
|
"yAxisLabel": "%",
|
||||||
|
"removeOlder": "1",
|
||||||
|
"removeOlderUnit": "3600",
|
||||||
|
"x": 1510,
|
||||||
|
"y": 400,
|
||||||
|
"wires": [],
|
||||||
|
"showLegend": true,
|
||||||
|
"category": "topic",
|
||||||
|
"categoryType": "msg",
|
||||||
|
"xAxisProperty": "",
|
||||||
|
"xAxisPropertyType": "timestamp",
|
||||||
|
"xAxisFormat": "",
|
||||||
|
"xAxisFormatType": "auto",
|
||||||
|
"yAxisProperty": "payload",
|
||||||
|
"yAxisPropertyType": "msg",
|
||||||
|
"xmin": "",
|
||||||
|
"xmax": "",
|
||||||
|
"ymin": "",
|
||||||
|
"ymax": "",
|
||||||
|
"bins": 10,
|
||||||
|
"action": "append",
|
||||||
|
"stackSeries": false,
|
||||||
|
"pointShape": "circle",
|
||||||
|
"pointRadius": 4,
|
||||||
|
"interpolation": "linear",
|
||||||
|
"className": "",
|
||||||
|
"colors": [
|
||||||
|
"#0095FF",
|
||||||
|
"#FF0000",
|
||||||
|
"#FF7F0E",
|
||||||
|
"#2CA02C",
|
||||||
|
"#A347E1",
|
||||||
|
"#D62728",
|
||||||
|
"#FF9896",
|
||||||
|
"#9467BD",
|
||||||
|
"#C5B0D5"
|
||||||
|
],
|
||||||
|
"textColor": [
|
||||||
|
"#666666"
|
||||||
|
],
|
||||||
|
"textColorDefault": true,
|
||||||
|
"gridColor": [
|
||||||
|
"#e5e5e5"
|
||||||
|
],
|
||||||
|
"gridColorDefault": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "rm_int_state_text",
|
||||||
|
"type": "ui-text",
|
||||||
|
"z": "12f41a7b538c40db",
|
||||||
|
"group": "ui_group_rm_int_vis",
|
||||||
|
"name": "State",
|
||||||
|
"label": "State Snapshot",
|
||||||
|
"order": 6,
|
||||||
|
"width": 12,
|
||||||
|
"height": 1,
|
||||||
|
"format": "{{msg.payload}}",
|
||||||
|
"layout": "row-spread",
|
||||||
|
"x": 1510,
|
||||||
|
"y": 460,
|
||||||
|
"wires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "rm_int_debug_influx",
|
||||||
|
"type": "debug",
|
||||||
|
"z": "12f41a7b538c40db",
|
||||||
|
"name": "Influx",
|
||||||
|
"active": true,
|
||||||
|
"tosidebar": true,
|
||||||
|
"complete": "true",
|
||||||
|
"targetType": "full",
|
||||||
|
"x": 1250,
|
||||||
|
"y": 520,
|
||||||
|
"wires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "rm_int_debug_parent",
|
||||||
|
"type": "debug",
|
||||||
|
"z": "12f41a7b538c40db",
|
||||||
|
"name": "Parent",
|
||||||
|
"active": true,
|
||||||
|
"tosidebar": true,
|
||||||
|
"complete": "true",
|
||||||
|
"targetType": "full",
|
||||||
|
"x": 1240,
|
||||||
|
"y": 560,
|
||||||
|
"wires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "rm_int_chart_ctrl",
|
||||||
|
"type": "ui-chart",
|
||||||
|
"z": "12f41a7b538c40db",
|
||||||
|
"group": "ui_group_rm_int_vis",
|
||||||
|
"name": "Ctrl",
|
||||||
|
"label": "Ctrl (%)",
|
||||||
|
"order": 4,
|
||||||
|
"width": 6,
|
||||||
|
"height": 4,
|
||||||
|
"chartType": "line",
|
||||||
|
"xAxisType": "time",
|
||||||
|
"yAxisLabel": "%",
|
||||||
|
"removeOlder": "1",
|
||||||
|
"removeOlderUnit": "3600",
|
||||||
|
"x": 1510,
|
||||||
|
"y": 460,
|
||||||
|
"wires": [],
|
||||||
|
"showLegend": true,
|
||||||
|
"category": "topic",
|
||||||
|
"categoryType": "msg",
|
||||||
|
"xAxisProperty": "",
|
||||||
|
"xAxisPropertyType": "timestamp",
|
||||||
|
"xAxisFormat": "",
|
||||||
|
"xAxisFormatType": "auto",
|
||||||
|
"yAxisProperty": "payload",
|
||||||
|
"yAxisPropertyType": "msg",
|
||||||
|
"xmin": "",
|
||||||
|
"xmax": "",
|
||||||
|
"ymin": "",
|
||||||
|
"ymax": "",
|
||||||
|
"bins": 10,
|
||||||
|
"action": "append",
|
||||||
|
"stackSeries": false,
|
||||||
|
"pointShape": "circle",
|
||||||
|
"pointRadius": 4,
|
||||||
|
"interpolation": "linear",
|
||||||
|
"className": "",
|
||||||
|
"colors": [
|
||||||
|
"#0095FF",
|
||||||
|
"#FF0000",
|
||||||
|
"#FF7F0E",
|
||||||
|
"#2CA02C",
|
||||||
|
"#A347E1",
|
||||||
|
"#D62728",
|
||||||
|
"#FF9896",
|
||||||
|
"#9467BD",
|
||||||
|
"#C5B0D5"
|
||||||
|
],
|
||||||
|
"textColor": [
|
||||||
|
"#666666"
|
||||||
|
],
|
||||||
|
"textColorDefault": true,
|
||||||
|
"gridColor": [
|
||||||
|
"#e5e5e5"
|
||||||
|
],
|
||||||
|
"gridColorDefault": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "rm_int_chart_statecode",
|
||||||
|
"type": "ui-chart",
|
||||||
|
"z": "12f41a7b538c40db",
|
||||||
|
"group": "ui_group_rm_int_vis",
|
||||||
|
"name": "State Code",
|
||||||
|
"label": "State Code (off=0 .. maint=9)",
|
||||||
|
"order": 5,
|
||||||
|
"width": 6,
|
||||||
|
"height": 4,
|
||||||
|
"chartType": "line",
|
||||||
|
"xAxisType": "time",
|
||||||
|
"yAxisLabel": "state",
|
||||||
|
"removeOlder": "1",
|
||||||
|
"removeOlderUnit": "3600",
|
||||||
|
"x": 1510,
|
||||||
|
"y": 520,
|
||||||
|
"wires": [],
|
||||||
|
"showLegend": true,
|
||||||
|
"category": "topic",
|
||||||
|
"categoryType": "msg",
|
||||||
|
"xAxisProperty": "",
|
||||||
|
"xAxisPropertyType": "timestamp",
|
||||||
|
"xAxisFormat": "",
|
||||||
|
"xAxisFormatType": "auto",
|
||||||
|
"yAxisProperty": "payload",
|
||||||
|
"yAxisPropertyType": "msg",
|
||||||
|
"xmin": "",
|
||||||
|
"xmax": "",
|
||||||
|
"ymin": "",
|
||||||
|
"ymax": "",
|
||||||
|
"bins": 10,
|
||||||
|
"action": "append",
|
||||||
|
"stackSeries": false,
|
||||||
|
"pointShape": "circle",
|
||||||
|
"pointRadius": 4,
|
||||||
|
"interpolation": "linear",
|
||||||
|
"className": "",
|
||||||
|
"colors": [
|
||||||
|
"#0095FF",
|
||||||
|
"#FF0000",
|
||||||
|
"#FF7F0E",
|
||||||
|
"#2CA02C",
|
||||||
|
"#A347E1",
|
||||||
|
"#D62728",
|
||||||
|
"#FF9896",
|
||||||
|
"#9467BD",
|
||||||
|
"#C5B0D5"
|
||||||
|
],
|
||||||
|
"textColor": [
|
||||||
|
"#666666"
|
||||||
|
],
|
||||||
|
"textColorDefault": true,
|
||||||
|
"gridColor": [
|
||||||
|
"#e5e5e5"
|
||||||
|
],
|
||||||
|
"gridColorDefault": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "rm_int_ctrl_setpoint_for_chart",
|
||||||
|
"type": "function",
|
||||||
|
"z": "12f41a7b538c40db",
|
||||||
|
"name": "ctrl setpoint series",
|
||||||
|
"func": "msg.topic = 'setpoint_ctrl';\nmsg.payload = Number(msg.payload);\nreturn Number.isFinite(msg.payload) ? msg : null;",
|
||||||
|
"outputs": 1,
|
||||||
|
"noerr": 0,
|
||||||
|
"initialize": "",
|
||||||
|
"finalize": "",
|
||||||
|
"libs": [],
|
||||||
|
"x": 560,
|
||||||
|
"y": 220,
|
||||||
|
"wires": [
|
||||||
|
[
|
||||||
|
"rm_int_chart_ctrl"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "rm_int_flow_setpoint_for_chart",
|
||||||
|
"type": "function",
|
||||||
|
"z": "12f41a7b538c40db",
|
||||||
|
"name": "flow setpoint series",
|
||||||
|
"func": "msg.topic = 'setpoint_flow';\nmsg.payload = Number(msg.payload);\nreturn Number.isFinite(msg.payload) ? msg : null;",
|
||||||
|
"outputs": 1,
|
||||||
|
"noerr": 0,
|
||||||
|
"initialize": "",
|
||||||
|
"finalize": "",
|
||||||
|
"libs": [],
|
||||||
|
"x": 570,
|
||||||
|
"y": 330,
|
||||||
|
"wires": [
|
||||||
|
[
|
||||||
|
"rm_int_chart_flow"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
"description": "Control module rotatingMachine",
|
"description": "Control module rotatingMachine",
|
||||||
"main": "rotatingMachine.js",
|
"main": "rotatingMachine.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "node rotatingMachine.js"
|
"test": "node --test test/basic/*.test.js test/integration/*.test.js test/edge/*.test.js"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|||||||
@@ -26,14 +26,21 @@
|
|||||||
cooldown: { value: 0 },
|
cooldown: { value: 0 },
|
||||||
movementMode : { value: "staticspeed" }, // static or dynamic
|
movementMode : { value: "staticspeed" }, // static or dynamic
|
||||||
machineCurve : { value: {}},
|
machineCurve : { value: {}},
|
||||||
|
processOutputFormat: { value: "process" },
|
||||||
|
dbaseOutputFormat: { value: "influxdb" },
|
||||||
|
|
||||||
//define asset properties
|
//define asset properties
|
||||||
uuid: { value: "" },
|
uuid: { value: "" },
|
||||||
|
assetTagNumber: { value: "" },
|
||||||
supplier: { value: "" },
|
supplier: { value: "" },
|
||||||
category: { value: "" },
|
category: { value: "" },
|
||||||
assetType: { value: "" },
|
assetType: { value: "" },
|
||||||
model: { value: "" },
|
model: { value: "" },
|
||||||
unit: { value: "" },
|
unit: { value: "" },
|
||||||
|
curvePressureUnit: { value: "mbar" },
|
||||||
|
curveFlowUnit: { value: "" },
|
||||||
|
curvePowerUnit: { value: "kW" },
|
||||||
|
curveControlUnit: { value: "%" },
|
||||||
|
|
||||||
//logger properties
|
//logger properties
|
||||||
enableLog: { value: false },
|
enableLog: { value: false },
|
||||||
@@ -55,7 +62,7 @@
|
|||||||
icon: "font-awesome/fa-cog",
|
icon: "font-awesome/fa-cog",
|
||||||
|
|
||||||
label: function () {
|
label: function () {
|
||||||
return this.positionIcon + " " + this.category || "Machine";
|
return (this.positionIcon || "") + " " + (this.category || "Machine");
|
||||||
},
|
},
|
||||||
|
|
||||||
oneditprepare: function() {
|
oneditprepare: function() {
|
||||||
@@ -143,6 +150,24 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<h3>Output Formats</h3>
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-processOutputFormat"><i class="fa fa-random"></i> Process Output</label>
|
||||||
|
<select id="node-input-processOutputFormat" style="width:60%;">
|
||||||
|
<option value="process">process</option>
|
||||||
|
<option value="json">json</option>
|
||||||
|
<option value="csv">csv</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-dbaseOutputFormat"><i class="fa fa-database"></i> Database Output</label>
|
||||||
|
<select id="node-input-dbaseOutputFormat" style="width:60%;">
|
||||||
|
<option value="influxdb">influxdb</option>
|
||||||
|
<option value="json">json</option>
|
||||||
|
<option value="csv">csv</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Asset fields injected here -->
|
<!-- Asset fields injected here -->
|
||||||
<div id="asset-fields-placeholder"></div>
|
<div id="asset-fields-placeholder"></div>
|
||||||
|
|
||||||
|
|||||||
211
src/nodeClass.js
211
src/nodeClass.js
@@ -4,7 +4,7 @@
|
|||||||
* Encapsulates all node logic in a reusable class. In future updates we can split this into multiple generic classes and use the config to specifiy which ones to use.
|
* Encapsulates all node logic in a reusable class. In future updates we can split this into multiple generic classes and use the config to specifiy which ones to use.
|
||||||
* This allows us to keep the Node-RED node clean and focused on wiring up the UI and event handlers.
|
* This allows us to keep the Node-RED node clean and focused on wiring up the UI and event handlers.
|
||||||
*/
|
*/
|
||||||
const { outputUtils, configManager } = require('generalFunctions');
|
const { outputUtils, configManager, convert } = require('generalFunctions');
|
||||||
const Specific = require("./specificClass");
|
const Specific = require("./specificClass");
|
||||||
|
|
||||||
class nodeClass {
|
class nodeClass {
|
||||||
@@ -21,6 +21,7 @@ class nodeClass {
|
|||||||
this.name = nameOfNode; // This is the name of the node, it should match the file name and the node type in Node-RED
|
this.name = nameOfNode; // This is the name of the node, it should match the file name and the node type in Node-RED
|
||||||
this.source = null; // Will hold the specific class instance
|
this.source = null; // Will hold the specific class instance
|
||||||
this.config = null; // Will hold the merged configuration
|
this.config = null; // Will hold the merged configuration
|
||||||
|
this._pressureInitWarned = false;
|
||||||
|
|
||||||
// Load default & UI config
|
// Load default & UI config
|
||||||
this._loadConfig(uiConfig,this.node);
|
this._loadConfig(uiConfig,this.node);
|
||||||
@@ -41,49 +42,74 @@ class nodeClass {
|
|||||||
* @param {object} uiConfig - Raw config from Node-RED UI.
|
* @param {object} uiConfig - Raw config from Node-RED UI.
|
||||||
*/
|
*/
|
||||||
_loadConfig(uiConfig,node) {
|
_loadConfig(uiConfig,node) {
|
||||||
|
const cfgMgr = new configManager();
|
||||||
// Merge UI config over defaults
|
const resolvedAssetUuid = uiConfig.assetUuid || uiConfig.uuid || null;
|
||||||
this.config = {
|
const resolvedAssetTagCode = uiConfig.assetTagCode || uiConfig.assetTagNumber || null;
|
||||||
general: {
|
const flowUnit = this._resolveUnitOrFallback(uiConfig.unit, 'volumeFlowRate', 'm3/h', 'flow');
|
||||||
id: node.id, // node.id is for the child registration process
|
const curveUnits = {
|
||||||
unit: uiConfig.unit, // add converter options later to convert to default units (need like a model that defines this which units we are going to use and then conver to those standards)
|
pressure: this._resolveUnitOrFallback(uiConfig.curvePressureUnit, 'pressure', 'mbar', 'curve pressure'),
|
||||||
logging: {
|
flow: this._resolveUnitOrFallback(uiConfig.curveFlowUnit || flowUnit, 'volumeFlowRate', flowUnit, 'curve flow'),
|
||||||
enabled: uiConfig.enableLog,
|
power: this._resolveUnitOrFallback(uiConfig.curvePowerUnit, 'power', 'kW', 'curve power'),
|
||||||
logLevel: uiConfig.logLevel
|
control: this._resolveControlUnitOrFallback(uiConfig.curveControlUnit, '%'),
|
||||||
}
|
|
||||||
},
|
|
||||||
asset: {
|
|
||||||
uuid: uiConfig.assetUuid, //need to add this later to the asset model
|
|
||||||
tagCode: uiConfig.assetTagCode, //need to add this later to the asset model
|
|
||||||
supplier: uiConfig.supplier,
|
|
||||||
category: uiConfig.category, //add later to define as the software type
|
|
||||||
type: uiConfig.assetType,
|
|
||||||
model: uiConfig.model,
|
|
||||||
unit: uiConfig.unit
|
|
||||||
},
|
|
||||||
functionality: {
|
|
||||||
positionVsParent: uiConfig.positionVsParent
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Build config: base sections + rotatingMachine-specific domain config
|
||||||
|
this.config = cfgMgr.buildConfig(this.name, uiConfig, node.id, {
|
||||||
|
flowNumber: uiConfig.flowNumber
|
||||||
|
});
|
||||||
|
|
||||||
|
// Override asset with rotatingMachine-specific fields
|
||||||
|
this.config.asset = {
|
||||||
|
...this.config.asset,
|
||||||
|
uuid: resolvedAssetUuid,
|
||||||
|
tagCode: resolvedAssetTagCode,
|
||||||
|
tagNumber: uiConfig.assetTagNumber || null,
|
||||||
|
unit: flowUnit,
|
||||||
|
curveUnits
|
||||||
|
};
|
||||||
|
|
||||||
|
// Ensure general unit uses resolved flow unit
|
||||||
|
this.config.general.unit = flowUnit;
|
||||||
|
|
||||||
// Utility for formatting outputs
|
// Utility for formatting outputs
|
||||||
this._output = new outputUtils();
|
this._output = new outputUtils();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_resolveUnitOrFallback(candidate, expectedMeasure, fallbackUnit, label) {
|
||||||
|
const raw = typeof candidate === 'string' ? candidate.trim() : '';
|
||||||
|
const fallback = String(fallbackUnit || '').trim();
|
||||||
|
if (!raw) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const desc = convert().describe(raw);
|
||||||
|
if (expectedMeasure && desc.measure !== expectedMeasure) {
|
||||||
|
throw new Error(`expected '${expectedMeasure}' but got '${desc.measure}'`);
|
||||||
|
}
|
||||||
|
return raw;
|
||||||
|
} catch (error) {
|
||||||
|
this.node?.warn?.(`Invalid ${label} unit '${raw}' (${error.message}). Falling back to '${fallback}'.`);
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_resolveControlUnitOrFallback(candidate, fallback = '%') {
|
||||||
|
const raw = typeof candidate === 'string' ? candidate.trim() : '';
|
||||||
|
return raw || fallback;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Instantiate the core Measurement logic and store as source.
|
* Instantiate the core Measurement logic and store as source.
|
||||||
*/
|
*/
|
||||||
_setupSpecificClass(uiConfig) {
|
_setupSpecificClass(uiConfig) {
|
||||||
const machineConfig = this.config;
|
const machineConfig = this.config;
|
||||||
|
|
||||||
console.log(`----------------> Loaded movementMode in nodeClass: ${uiConfig.movementMode}`);
|
|
||||||
|
|
||||||
// need extra state for this
|
// need extra state for this
|
||||||
const stateConfig = {
|
const stateConfig = {
|
||||||
general: {
|
general: {
|
||||||
logging: {
|
logging: {
|
||||||
enabled: machineConfig.eneableLog,
|
enabled: machineConfig.general.logging.enabled,
|
||||||
logLevel: machineConfig.logLevel
|
logLevel: machineConfig.general.logging.logLevel
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
movement: {
|
movement: {
|
||||||
@@ -117,8 +143,25 @@ class nodeClass {
|
|||||||
try {
|
try {
|
||||||
const mode = m.currentMode;
|
const mode = m.currentMode;
|
||||||
const state = m.state.getCurrentState();
|
const state = m.state.getCurrentState();
|
||||||
const flow = Math.round(m.measurements.type("flow").variant("predicted").position('downstream').getCurrentValue('m3/h'));
|
const requiresPressurePrediction = ["operational", "warmingup", "accelerating", "decelerating"].includes(state);
|
||||||
const power = Math.round(m.measurements.type("power").variant("predicted").position('atequipment').getCurrentValue('kW'));
|
const pressureStatus = typeof m.getPressureInitializationStatus === "function"
|
||||||
|
? m.getPressureInitializationStatus()
|
||||||
|
: { initialized: true };
|
||||||
|
|
||||||
|
if (requiresPressurePrediction && !pressureStatus.initialized) {
|
||||||
|
if (!this._pressureInitWarned) {
|
||||||
|
this.node.warn("Pressure input is not initialized (upstream/downstream missing). Predictions are using minimum pressure.");
|
||||||
|
this._pressureInitWarned = true;
|
||||||
|
}
|
||||||
|
return { fill: "yellow", shape: "ring", text: `${mode}: pressure not initialized` };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pressureStatus.initialized) {
|
||||||
|
this._pressureInitWarned = false;
|
||||||
|
}
|
||||||
|
const flowUnit = m?.config?.general?.unit || 'm3/h';
|
||||||
|
const flow = Math.round(m.measurements.type("flow").variant("predicted").position('downstream').getCurrentValue(flowUnit));
|
||||||
|
const power = Math.round(m.measurements.type("power").variant("predicted").position('atEquipment').getCurrentValue('kW'));
|
||||||
let symbolState;
|
let symbolState;
|
||||||
switch(state){
|
switch(state){
|
||||||
case "off":
|
case "off":
|
||||||
@@ -164,16 +207,16 @@ class nodeClass {
|
|||||||
status = { fill: "blue", shape: "dot", text: `${mode}: ${symbolState}` };
|
status = { fill: "blue", shape: "dot", text: `${mode}: ${symbolState}` };
|
||||||
break;
|
break;
|
||||||
case "operational":
|
case "operational":
|
||||||
status = { fill: "green", shape: "dot", text: `${mode}: ${symbolState} | ${roundedPosition}% | 💨${flow}m³/h | ⚡${power}kW` };
|
status = { fill: "green", shape: "dot", text: `${mode}: ${symbolState} | ${roundedPosition}% | 💨${flow}${flowUnit} | ⚡${power}kW` };
|
||||||
break;
|
break;
|
||||||
case "starting":
|
case "starting":
|
||||||
status = { fill: "yellow", shape: "dot", text: `${mode}: ${symbolState}` };
|
status = { fill: "yellow", shape: "dot", text: `${mode}: ${symbolState}` };
|
||||||
break;
|
break;
|
||||||
case "warmingup":
|
case "warmingup":
|
||||||
status = { fill: "green", shape: "dot", text: `${mode}: ${symbolState} | ${roundedPosition}% | 💨${flow}m³/h | ⚡${power}kW` };
|
status = { fill: "green", shape: "dot", text: `${mode}: ${symbolState} | ${roundedPosition}% | 💨${flow}${flowUnit} | ⚡${power}kW` };
|
||||||
break;
|
break;
|
||||||
case "accelerating":
|
case "accelerating":
|
||||||
status = { fill: "yellow", shape: "dot", text: `${mode}: ${symbolState} | ${roundedPosition}%| 💨${flow}m³/h | ⚡${power}kW` };
|
status = { fill: "yellow", shape: "dot", text: `${mode}: ${symbolState} | ${roundedPosition}%| 💨${flow}${flowUnit} | ⚡${power}kW` };
|
||||||
break;
|
break;
|
||||||
case "stopping":
|
case "stopping":
|
||||||
status = { fill: "yellow", shape: "dot", text: `${mode}: ${symbolState}` };
|
status = { fill: "yellow", shape: "dot", text: `${mode}: ${symbolState}` };
|
||||||
@@ -182,14 +225,14 @@ class nodeClass {
|
|||||||
status = { fill: "yellow", shape: "dot", text: `${mode}: ${symbolState}` };
|
status = { fill: "yellow", shape: "dot", text: `${mode}: ${symbolState}` };
|
||||||
break;
|
break;
|
||||||
case "decelerating":
|
case "decelerating":
|
||||||
status = { fill: "yellow", shape: "dot", text: `${mode}: ${symbolState} - ${roundedPosition}% | 💨${flow}m³/h | ⚡${power}kW` };
|
status = { fill: "yellow", shape: "dot", text: `${mode}: ${symbolState} - ${roundedPosition}% | 💨${flow}${flowUnit} | ⚡${power}kW` };
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
status = { fill: "grey", shape: "dot", text: `${mode}: ${symbolState}` };
|
status = { fill: "grey", shape: "dot", text: `${mode}: ${symbolState}` };
|
||||||
}
|
}
|
||||||
return status;
|
return status;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
node.error("Error in updateNodeStatus: " + error.message);
|
this.node.error("Error in updateNodeStatus: " + error.message);
|
||||||
return { fill: "red", shape: "ring", text: "Status Error" };
|
return { fill: "red", shape: "ring", text: "Status Error" };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -202,7 +245,7 @@ class nodeClass {
|
|||||||
this.node.send([
|
this.node.send([
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
{ topic: 'registerChild', payload: this.config.general.id, positionVsParent: this.config?.functionality?.positionVsParent || 'atEquipment' },
|
{ topic: 'registerChild', payload: this.node.id, positionVsParent: this.config?.functionality?.positionVsParent || 'atEquipment' },
|
||||||
]);
|
]);
|
||||||
}, 100);
|
}, 100);
|
||||||
}
|
}
|
||||||
@@ -234,7 +277,7 @@ class nodeClass {
|
|||||||
const influxMsg = this._output.formatMsg(raw, this.source.config, 'influxdb');
|
const influxMsg = this._output.formatMsg(raw, this.source.config, 'influxdb');
|
||||||
|
|
||||||
// Send only updated outputs on ports 0 & 1
|
// Send only updated outputs on ports 0 & 1
|
||||||
this.node.send([processMsg, influxMsg]);
|
this.node.send([processMsg, influxMsg, null]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -244,42 +287,114 @@ class nodeClass {
|
|||||||
this.node.on('input', (msg, send, done) => {
|
this.node.on('input', (msg, send, done) => {
|
||||||
/* Update to complete event based node by putting the tick function after an input event */
|
/* Update to complete event based node by putting the tick function after an input event */
|
||||||
const m = this.source;
|
const m = this.source;
|
||||||
|
const nodeSend = typeof send === 'function' ? send : (outMsg) => this.node.send(outMsg);
|
||||||
|
|
||||||
|
try {
|
||||||
switch(msg.topic) {
|
switch(msg.topic) {
|
||||||
case 'registerChild':
|
case 'registerChild': {
|
||||||
// Register this node as a child of the parent node
|
// Register this node as a child of the parent node
|
||||||
const childId = msg.payload;
|
const childId = msg.payload;
|
||||||
const childObj = this.RED.nodes.getNode(childId);
|
const childObj = this.RED.nodes.getNode(childId);
|
||||||
|
if (!childObj || !childObj.source) {
|
||||||
|
this.node.warn(`registerChild failed: child '${childId}' not found or has no source`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
m.childRegistrationUtils.registerChild(childObj.source ,msg.positionVsParent);
|
m.childRegistrationUtils.registerChild(childObj.source ,msg.positionVsParent);
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
case 'setMode':
|
case 'setMode':
|
||||||
m.setMode(msg.payload);
|
m.setMode(msg.payload);
|
||||||
break;
|
break;
|
||||||
case 'execSequence':
|
case 'execSequence': {
|
||||||
const { source, action, parameter } = msg.payload;
|
const { source, action, parameter } = msg.payload;
|
||||||
m.handleInput(source, action, parameter);
|
m.handleInput(source, action, parameter);
|
||||||
break;
|
break;
|
||||||
case 'execMovement':
|
}
|
||||||
|
case 'execMovement': {
|
||||||
const { source: mvSource, action: mvAction, setpoint } = msg.payload;
|
const { source: mvSource, action: mvAction, setpoint } = msg.payload;
|
||||||
m.handleInput(mvSource, mvAction, Number(setpoint));
|
m.handleInput(mvSource, mvAction, Number(setpoint));
|
||||||
break;
|
break;
|
||||||
case 'flowMovement':
|
}
|
||||||
|
case 'flowMovement': {
|
||||||
const { source: fmSource, action: fmAction, setpoint: fmSetpoint } = msg.payload;
|
const { source: fmSource, action: fmAction, setpoint: fmSetpoint } = msg.payload;
|
||||||
m.handleInput(fmSource, fmAction, Number(fmSetpoint));
|
m.handleInput(fmSource, fmAction, Number(fmSetpoint));
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case 'emergencystop':
|
}
|
||||||
|
case 'emergencystop': {
|
||||||
const { source: esSource, action: esAction } = msg.payload;
|
const { source: esSource, action: esAction } = msg.payload;
|
||||||
m.handleInput(esSource, esAction);
|
m.handleInput(esSource, esAction);
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
|
case 'simulateMeasurement':
|
||||||
|
{
|
||||||
|
const payload = msg.payload || {};
|
||||||
|
const type = String(payload.type || '').toLowerCase();
|
||||||
|
const position = payload.position || 'atEquipment';
|
||||||
|
const value = Number(payload.value);
|
||||||
|
const unit = typeof payload.unit === 'string' ? payload.unit.trim() : '';
|
||||||
|
const supportedTypes = new Set(['pressure', 'flow', 'temperature', 'power']);
|
||||||
|
const context = {
|
||||||
|
timestamp: payload.timestamp || Date.now(),
|
||||||
|
unit,
|
||||||
|
childName: 'dashboard-sim',
|
||||||
|
childId: 'dashboard-sim',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!Number.isFinite(value)) {
|
||||||
|
this.node.warn('simulateMeasurement payload.value must be a finite number');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!supportedTypes.has(type)) {
|
||||||
|
this.node.warn(`Unsupported simulateMeasurement type: ${type}`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!unit) {
|
||||||
|
this.node.warn('simulateMeasurement payload.unit is required');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof m.isUnitValidForType === 'function' && !m.isUnitValidForType(type, unit)) {
|
||||||
|
this.node.warn(`simulateMeasurement payload.unit '${unit}' is invalid for type '${type}'`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case 'pressure':
|
||||||
|
if (typeof m.updateSimulatedMeasurement === "function") {
|
||||||
|
m.updateSimulatedMeasurement(type, position, value, context);
|
||||||
|
} else {
|
||||||
|
m.updateMeasuredPressure(value, position, context);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'flow':
|
||||||
|
m.updateMeasuredFlow(value, position, context);
|
||||||
|
break;
|
||||||
|
case 'temperature':
|
||||||
|
m.updateMeasuredTemperature(value, position, context);
|
||||||
|
break;
|
||||||
|
case 'power':
|
||||||
|
m.updateMeasuredPower(value, position, context);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
case 'showWorkingCurves':
|
case 'showWorkingCurves':
|
||||||
m.showWorkingCurves();
|
nodeSend([{ ...msg, topic : "showWorkingCurves" , payload: m.showWorkingCurves() }, null, null]);
|
||||||
send({ topic : "Showing curve" , payload: m.showWorkingCurves() });
|
|
||||||
break;
|
break;
|
||||||
case 'CoG':
|
case 'CoG':
|
||||||
m.showCoG();
|
nodeSend([{ ...msg, topic : "showCoG" , payload: m.showCoG() }, null, null]);
|
||||||
send({ topic : "Showing CoG" , payload: m.showCoG() });
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
if (typeof done === 'function') done();
|
||||||
|
} catch (error) {
|
||||||
|
if (typeof done === 'function') {
|
||||||
|
done(error);
|
||||||
|
} else {
|
||||||
|
this.node.error(error, msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -290,7 +405,7 @@ class nodeClass {
|
|||||||
this.node.on('close', (done) => {
|
this.node.on('close', (done) => {
|
||||||
clearInterval(this._tickInterval);
|
clearInterval(this._tickInterval);
|
||||||
clearInterval(this._statusInterval);
|
clearInterval(this._statusInterval);
|
||||||
done();
|
if (typeof done === 'function') done();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
1071
src/specificClass.js
1071
src/specificClass.js
File diff suppressed because it is too large
Load Diff
31
test/basic/constructor.basic.test.js
Normal file
31
test/basic/constructor.basic.test.js
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
const test = require('node:test');
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
|
||||||
|
const Machine = require('../../src/specificClass');
|
||||||
|
const { makeMachineConfig, makeStateConfig } = require('../helpers/factories');
|
||||||
|
|
||||||
|
test('constructor initializes with valid curve model', () => {
|
||||||
|
const machine = new Machine(makeMachineConfig(), makeStateConfig());
|
||||||
|
assert.equal(machine.hasCurve, true);
|
||||||
|
assert.ok(machine.predictFlow);
|
||||||
|
assert.ok(machine.predictPower);
|
||||||
|
assert.ok(machine.predictCtrl);
|
||||||
|
|
||||||
|
const out = machine.getOutput();
|
||||||
|
assert.ok('state' in out);
|
||||||
|
assert.ok('mode' in out);
|
||||||
|
assert.ok('ctrl' in out);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('constructor handles missing curve model without throwing', () => {
|
||||||
|
const cfg = makeMachineConfig({ asset: { supplier: 'x', category: 'machine', type: 'pump', model: 'not-existing-model', unit: 'm3/h' } });
|
||||||
|
const machine = new Machine(cfg, makeStateConfig());
|
||||||
|
|
||||||
|
assert.equal(machine.hasCurve, false);
|
||||||
|
assert.equal(machine.predictFlow, null);
|
||||||
|
assert.equal(machine.predictPower, null);
|
||||||
|
assert.equal(machine.predictCtrl, null);
|
||||||
|
|
||||||
|
const out = machine.getOutput();
|
||||||
|
assert.ok('state' in out);
|
||||||
|
});
|
||||||
44
test/basic/mode-and-input.basic.test.js
Normal file
44
test/basic/mode-and-input.basic.test.js
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
const test = require('node:test');
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
|
||||||
|
const Machine = require('../../src/specificClass');
|
||||||
|
const { makeMachineConfig, makeStateConfig } = require('../helpers/factories');
|
||||||
|
|
||||||
|
test('setMode changes mode only for allowed values', () => {
|
||||||
|
const machine = new Machine(makeMachineConfig(), makeStateConfig());
|
||||||
|
const original = machine.currentMode;
|
||||||
|
|
||||||
|
machine.setMode('virtualControl');
|
||||||
|
assert.equal(machine.currentMode, 'virtualControl');
|
||||||
|
|
||||||
|
machine.setMode('invalid-mode');
|
||||||
|
assert.equal(machine.currentMode, 'virtualControl');
|
||||||
|
assert.notEqual(machine.currentMode, original);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handleInput rejects non-string action safely', async () => {
|
||||||
|
const machine = new Machine(makeMachineConfig(), makeStateConfig());
|
||||||
|
await assert.doesNotReject(async () => {
|
||||||
|
await machine.handleInput('GUI', 123, null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handleInput ignores disallowed source/action combination', async () => {
|
||||||
|
const machine = new Machine(makeMachineConfig(), makeStateConfig());
|
||||||
|
machine.setMode('fysicalControl');
|
||||||
|
|
||||||
|
const before = machine.state.getCurrentState();
|
||||||
|
await machine.handleInput('GUI', 'execSequence', 'startup');
|
||||||
|
const after = machine.state.getCurrentState();
|
||||||
|
|
||||||
|
assert.equal(before, after);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('warmingup is treated as active for prediction updates', () => {
|
||||||
|
const machine = new Machine(
|
||||||
|
makeMachineConfig(),
|
||||||
|
makeStateConfig({ state: { current: 'warmingup' } })
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(machine._isOperationalState(), true);
|
||||||
|
});
|
||||||
109
test/basic/nodeClass-config.basic.test.js
Normal file
109
test/basic/nodeClass-config.basic.test.js
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
const test = require('node:test');
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
|
||||||
|
const NodeClass = require('../../src/nodeClass');
|
||||||
|
const { makeNodeStub } = require('../helpers/factories');
|
||||||
|
|
||||||
|
function makeUiConfig(overrides = {}) {
|
||||||
|
return {
|
||||||
|
unit: 'm3/h',
|
||||||
|
enableLog: true,
|
||||||
|
logLevel: 'debug',
|
||||||
|
supplier: 'hidrostal',
|
||||||
|
category: 'machine',
|
||||||
|
assetType: 'pump',
|
||||||
|
model: 'hidrostal-H05K-S03R',
|
||||||
|
curvePressureUnit: 'mbar',
|
||||||
|
curveFlowUnit: 'm3/h',
|
||||||
|
curvePowerUnit: 'kW',
|
||||||
|
curveControlUnit: '%',
|
||||||
|
positionVsParent: 'atEquipment',
|
||||||
|
speed: 1,
|
||||||
|
movementMode: 'staticspeed',
|
||||||
|
startup: 0,
|
||||||
|
warmup: 0,
|
||||||
|
shutdown: 0,
|
||||||
|
cooldown: 0,
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
test('_loadConfig maps legacy editor fields for asset identity', () => {
|
||||||
|
const inst = Object.create(NodeClass.prototype);
|
||||||
|
inst.node = makeNodeStub();
|
||||||
|
inst.name = 'rotatingMachine';
|
||||||
|
|
||||||
|
inst._loadConfig(
|
||||||
|
makeUiConfig({
|
||||||
|
uuid: 'uuid-from-editor',
|
||||||
|
assetTagNumber: 'TAG-123',
|
||||||
|
}),
|
||||||
|
inst.node
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(inst.config.asset.uuid, 'uuid-from-editor');
|
||||||
|
assert.equal(inst.config.asset.tagCode, 'TAG-123');
|
||||||
|
assert.equal(inst.config.asset.tagNumber, 'TAG-123');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('_loadConfig prefers explicit assetUuid/assetTagCode when present', () => {
|
||||||
|
const inst = Object.create(NodeClass.prototype);
|
||||||
|
inst.node = makeNodeStub();
|
||||||
|
inst.name = 'rotatingMachine';
|
||||||
|
|
||||||
|
inst._loadConfig(
|
||||||
|
makeUiConfig({
|
||||||
|
uuid: 'legacy-uuid',
|
||||||
|
assetUuid: 'explicit-uuid',
|
||||||
|
assetTagNumber: 'legacy-tag',
|
||||||
|
assetTagCode: 'explicit-tag',
|
||||||
|
}),
|
||||||
|
inst.node
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(inst.config.asset.uuid, 'explicit-uuid');
|
||||||
|
assert.equal(inst.config.asset.tagCode, 'explicit-tag');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('_loadConfig builds explicit curveUnits and falls back for invalid flow unit', () => {
|
||||||
|
const inst = Object.create(NodeClass.prototype);
|
||||||
|
inst.node = makeNodeStub();
|
||||||
|
inst.name = 'rotatingMachine';
|
||||||
|
|
||||||
|
inst._loadConfig(
|
||||||
|
makeUiConfig({
|
||||||
|
unit: 'not-a-unit',
|
||||||
|
curvePressureUnit: 'mbar',
|
||||||
|
curveFlowUnit: 'm3/h',
|
||||||
|
curvePowerUnit: 'kW',
|
||||||
|
curveControlUnit: '%',
|
||||||
|
}),
|
||||||
|
inst.node
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(inst.config.general.unit, 'm3/h');
|
||||||
|
assert.equal(inst.config.asset.unit, 'm3/h');
|
||||||
|
assert.equal(inst.config.asset.curveUnits.pressure, 'mbar');
|
||||||
|
assert.equal(inst.config.asset.curveUnits.flow, 'm3/h');
|
||||||
|
assert.equal(inst.config.asset.curveUnits.power, 'kW');
|
||||||
|
assert.equal(inst.config.asset.curveUnits.control, '%');
|
||||||
|
assert.ok(inst.node._warns.length >= 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('_setupSpecificClass propagates logging settings into state config', () => {
|
||||||
|
const inst = Object.create(NodeClass.prototype);
|
||||||
|
inst.node = makeNodeStub();
|
||||||
|
inst.name = 'rotatingMachine';
|
||||||
|
const uiConfig = makeUiConfig({
|
||||||
|
enableLog: true,
|
||||||
|
logLevel: 'warn',
|
||||||
|
uuid: 'uuid-test',
|
||||||
|
assetTagNumber: 'TAG-9',
|
||||||
|
});
|
||||||
|
|
||||||
|
inst._loadConfig(uiConfig, inst.node);
|
||||||
|
inst._setupSpecificClass(uiConfig);
|
||||||
|
|
||||||
|
assert.equal(inst.source.state.config.general.logging.enabled, true);
|
||||||
|
assert.equal(inst.source.state.config.general.logging.logLevel, 'warn');
|
||||||
|
});
|
||||||
74
test/edge/error-paths.edge.test.js
Normal file
74
test/edge/error-paths.edge.test.js
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
const test = require('node:test');
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
|
||||||
|
const Machine = require('../../src/specificClass');
|
||||||
|
const NodeClass = require('../../src/nodeClass');
|
||||||
|
const { makeMachineConfig, makeStateConfig, makeNodeStub } = require('../helpers/factories');
|
||||||
|
|
||||||
|
test('setpoint rejects negative inputs without throwing', async () => {
|
||||||
|
const machine = new Machine(makeMachineConfig(), makeStateConfig({ state: { current: 'operational' } }));
|
||||||
|
await assert.doesNotReject(async () => {
|
||||||
|
await machine.setpoint(-1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('setpoint is constrained to safe movement/curve bounds', async () => {
|
||||||
|
const machine = new Machine(makeMachineConfig(), makeStateConfig({ state: { current: 'operational' } }));
|
||||||
|
const requested = [];
|
||||||
|
machine.state.moveTo = async (target) => {
|
||||||
|
requested.push(target);
|
||||||
|
};
|
||||||
|
|
||||||
|
const stateMin = machine.state.movementManager.minPosition;
|
||||||
|
const stateMax = machine.state.movementManager.maxPosition;
|
||||||
|
const curveMin = machine.predictFlow.currentFxyXMin;
|
||||||
|
const curveMax = machine.predictFlow.currentFxyXMax;
|
||||||
|
const min = Math.max(stateMin, curveMin);
|
||||||
|
const max = Math.min(stateMax, curveMax);
|
||||||
|
|
||||||
|
await machine.setpoint(min - 100);
|
||||||
|
await machine.setpoint(max + 100);
|
||||||
|
|
||||||
|
assert.equal(requested.length, 2);
|
||||||
|
assert.equal(requested[0], min);
|
||||||
|
assert.equal(requested[1], max);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('nodeClass _updateNodeStatus returns error status on internal failure', () => {
|
||||||
|
const inst = Object.create(NodeClass.prototype);
|
||||||
|
const node = makeNodeStub();
|
||||||
|
inst.node = node;
|
||||||
|
inst.source = {
|
||||||
|
currentMode: 'auto',
|
||||||
|
state: {
|
||||||
|
getCurrentState() {
|
||||||
|
throw new Error('boom');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const status = inst._updateNodeStatus();
|
||||||
|
assert.equal(status.text, 'Status Error');
|
||||||
|
assert.equal(node._errors.length, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('measurement handlers reject incompatible units', () => {
|
||||||
|
const machine = new Machine(makeMachineConfig(), makeStateConfig({ state: { current: 'operational' } }));
|
||||||
|
|
||||||
|
assert.equal(machine.isUnitValidForType('flow', 'm3/h'), true);
|
||||||
|
assert.equal(machine.isUnitValidForType('flow', 'mbar'), false);
|
||||||
|
|
||||||
|
machine.updateMeasuredFlow(100, 'downstream', {
|
||||||
|
timestamp: Date.now(),
|
||||||
|
unit: 'mbar',
|
||||||
|
childName: 'bad-ft',
|
||||||
|
});
|
||||||
|
|
||||||
|
const measuredFlow = machine.measurements
|
||||||
|
.type('flow')
|
||||||
|
.variant('measured')
|
||||||
|
.position('downstream')
|
||||||
|
.getCurrentValue();
|
||||||
|
|
||||||
|
assert.equal(measuredFlow, null);
|
||||||
|
});
|
||||||
187
test/edge/nodeClass-routing.edge.test.js
Normal file
187
test/edge/nodeClass-routing.edge.test.js
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
const test = require('node:test');
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
|
||||||
|
const NodeClass = require('../../src/nodeClass');
|
||||||
|
const { makeNodeStub, makeREDStub } = require('../helpers/factories');
|
||||||
|
|
||||||
|
test('input handler routes topics to source methods', () => {
|
||||||
|
const inst = Object.create(NodeClass.prototype);
|
||||||
|
const node = makeNodeStub();
|
||||||
|
|
||||||
|
const calls = [];
|
||||||
|
inst.node = node;
|
||||||
|
inst.RED = makeREDStub({
|
||||||
|
child1: {
|
||||||
|
source: { id: 'child-source' },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
inst.source = {
|
||||||
|
childRegistrationUtils: {
|
||||||
|
registerChild(childSource, pos) {
|
||||||
|
calls.push(['registerChild', childSource, pos]);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setMode(mode) {
|
||||||
|
calls.push(['setMode', mode]);
|
||||||
|
},
|
||||||
|
handleInput(source, action, parameter) {
|
||||||
|
calls.push(['handleInput', source, action, parameter]);
|
||||||
|
},
|
||||||
|
showWorkingCurves() {
|
||||||
|
return { ok: true };
|
||||||
|
},
|
||||||
|
showCoG() {
|
||||||
|
return { cog: 1 };
|
||||||
|
},
|
||||||
|
updateSimulatedMeasurement(type, position, value) {
|
||||||
|
calls.push(['updateSimulatedMeasurement', type, position, value]);
|
||||||
|
},
|
||||||
|
updateMeasuredPressure(value, position) {
|
||||||
|
calls.push(['updateMeasuredPressure', value, position]);
|
||||||
|
},
|
||||||
|
updateMeasuredFlow(value, position) {
|
||||||
|
calls.push(['updateMeasuredFlow', value, position]);
|
||||||
|
},
|
||||||
|
updateMeasuredPower(value, position) {
|
||||||
|
calls.push(['updateMeasuredPower', value, position]);
|
||||||
|
},
|
||||||
|
updateMeasuredTemperature(value, position) {
|
||||||
|
calls.push(['updateMeasuredTemperature', value, position]);
|
||||||
|
},
|
||||||
|
isUnitValidForType() {
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
inst._attachInputHandler();
|
||||||
|
const onInput = node._handlers.input;
|
||||||
|
|
||||||
|
onInput({ topic: 'setMode', payload: 'auto' }, () => {}, () => {});
|
||||||
|
onInput({ topic: 'execSequence', payload: { source: 'GUI', action: 'execSequence', parameter: 'startup' } }, () => {}, () => {});
|
||||||
|
onInput({ topic: 'flowMovement', payload: { source: 'GUI', action: 'flowMovement', setpoint: 123 } }, () => {}, () => {});
|
||||||
|
onInput({ topic: 'emergencystop', payload: { source: 'GUI', action: 'emergencystop' } }, () => {}, () => {});
|
||||||
|
onInput({ topic: 'registerChild', payload: 'child1', positionVsParent: 'downstream' }, () => {}, () => {});
|
||||||
|
onInput({ topic: 'simulateMeasurement', payload: { type: 'pressure', position: 'upstream', value: 250, unit: 'mbar' } }, () => {}, () => {});
|
||||||
|
onInput({ topic: 'simulateMeasurement', payload: { type: 'power', position: 'atEquipment', value: 7.5, unit: 'kW' } }, () => {}, () => {});
|
||||||
|
|
||||||
|
assert.deepEqual(calls[0], ['setMode', 'auto']);
|
||||||
|
assert.deepEqual(calls[1], ['handleInput', 'GUI', 'execSequence', 'startup']);
|
||||||
|
assert.deepEqual(calls[2], ['handleInput', 'GUI', 'flowMovement', 123]);
|
||||||
|
assert.deepEqual(calls[3], ['handleInput', 'GUI', 'emergencystop', undefined]);
|
||||||
|
assert.deepEqual(calls[4], ['registerChild', { id: 'child-source' }, 'downstream']);
|
||||||
|
assert.deepEqual(calls[5], ['updateSimulatedMeasurement', 'pressure', 'upstream', 250]);
|
||||||
|
assert.deepEqual(calls[6], ['updateMeasuredPower', 7.5, 'atEquipment']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('simulateMeasurement warns and ignores invalid payloads', () => {
|
||||||
|
const inst = Object.create(NodeClass.prototype);
|
||||||
|
const node = makeNodeStub();
|
||||||
|
|
||||||
|
const calls = [];
|
||||||
|
inst.node = node;
|
||||||
|
inst.RED = makeREDStub();
|
||||||
|
inst.source = {
|
||||||
|
childRegistrationUtils: { registerChild() {} },
|
||||||
|
setMode() {},
|
||||||
|
handleInput() {},
|
||||||
|
showWorkingCurves() { return {}; },
|
||||||
|
showCoG() { return {}; },
|
||||||
|
updateSimulatedMeasurement() { calls.push('updateSimulatedMeasurement'); },
|
||||||
|
updateMeasuredPressure() { calls.push('updateMeasuredPressure'); },
|
||||||
|
updateMeasuredFlow() { calls.push('updateMeasuredFlow'); },
|
||||||
|
updateMeasuredPower() { calls.push('updateMeasuredPower'); },
|
||||||
|
updateMeasuredTemperature() { calls.push('updateMeasuredTemperature'); },
|
||||||
|
};
|
||||||
|
|
||||||
|
inst._attachInputHandler();
|
||||||
|
const onInput = node._handlers.input;
|
||||||
|
|
||||||
|
onInput({ topic: 'simulateMeasurement', payload: { type: 'pressure', position: 'upstream', value: 'not-a-number' } }, () => {}, () => {});
|
||||||
|
onInput({ topic: 'simulateMeasurement', payload: { type: 'flow', position: 'upstream', value: 12 } }, () => {}, () => {});
|
||||||
|
onInput({ topic: 'simulateMeasurement', payload: { type: 'unknown', position: 'upstream', value: 12, unit: 'm3/h' } }, () => {}, () => {});
|
||||||
|
|
||||||
|
assert.equal(calls.length, 0);
|
||||||
|
assert.equal(node._warns.length, 3);
|
||||||
|
assert.match(String(node._warns[0]), /finite number/i);
|
||||||
|
assert.match(String(node._warns[1]), /payload\.unit is required/i);
|
||||||
|
assert.match(String(node._warns[2]), /unsupported simulatemeasurement type/i);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('status shows warning when pressure inputs are not initialized', () => {
|
||||||
|
const inst = Object.create(NodeClass.prototype);
|
||||||
|
const node = makeNodeStub();
|
||||||
|
|
||||||
|
inst.node = node;
|
||||||
|
inst.source = {
|
||||||
|
currentMode: 'virtualControl',
|
||||||
|
state: {
|
||||||
|
getCurrentState() {
|
||||||
|
return 'operational';
|
||||||
|
},
|
||||||
|
getCurrentPosition() {
|
||||||
|
return 50;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
getPressureInitializationStatus() {
|
||||||
|
return { initialized: false, hasUpstream: false, hasDownstream: false, hasDifferential: false };
|
||||||
|
},
|
||||||
|
measurements: {
|
||||||
|
type() {
|
||||||
|
return {
|
||||||
|
variant() {
|
||||||
|
return {
|
||||||
|
position() {
|
||||||
|
return { getCurrentValue() { return 0; } };
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const status = inst._updateNodeStatus();
|
||||||
|
const statusAgain = inst._updateNodeStatus();
|
||||||
|
|
||||||
|
assert.equal(status.fill, 'yellow');
|
||||||
|
assert.equal(status.shape, 'ring');
|
||||||
|
assert.match(status.text, /pressure not initialized/i);
|
||||||
|
assert.equal(statusAgain.fill, 'yellow');
|
||||||
|
assert.equal(node._warns.length, 1);
|
||||||
|
assert.match(String(node._warns[0]), /Pressure input is not initialized/i);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('showWorkingCurves and CoG route reply messages to process output index', () => {
|
||||||
|
const inst = Object.create(NodeClass.prototype);
|
||||||
|
const node = makeNodeStub();
|
||||||
|
inst.node = node;
|
||||||
|
inst.RED = makeREDStub();
|
||||||
|
inst.source = {
|
||||||
|
childRegistrationUtils: { registerChild() {} },
|
||||||
|
setMode() {},
|
||||||
|
handleInput() {},
|
||||||
|
showWorkingCurves() {
|
||||||
|
return { curve: [1, 2, 3] };
|
||||||
|
},
|
||||||
|
showCoG() {
|
||||||
|
return { cog: 0.77 };
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
inst._attachInputHandler();
|
||||||
|
const onInput = node._handlers.input;
|
||||||
|
const sent = [];
|
||||||
|
const send = (out) => sent.push(out);
|
||||||
|
|
||||||
|
onInput({ topic: 'showWorkingCurves', payload: { request: true } }, send, () => {});
|
||||||
|
onInput({ topic: 'CoG', payload: { request: true } }, send, () => {});
|
||||||
|
|
||||||
|
assert.equal(sent.length, 2);
|
||||||
|
assert.equal(Array.isArray(sent[0]), true);
|
||||||
|
assert.equal(sent[0].length, 3);
|
||||||
|
assert.equal(sent[0][0].topic, 'showWorkingCurves');
|
||||||
|
assert.equal(sent[0][1], null);
|
||||||
|
assert.equal(sent[0][2], null);
|
||||||
|
assert.equal(sent[1][0].topic, 'showCoG');
|
||||||
|
});
|
||||||
116
test/helpers/factories.js
Normal file
116
test/helpers/factories.js
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
const { MeasurementContainer } = require('generalFunctions');
|
||||||
|
|
||||||
|
function makeMachineConfig(overrides = {}) {
|
||||||
|
return {
|
||||||
|
general: {
|
||||||
|
id: 'rm-test-1',
|
||||||
|
name: 'rotating-machine-test',
|
||||||
|
unit: 'm3/h',
|
||||||
|
logging: { enabled: false, logLevel: 'error' },
|
||||||
|
},
|
||||||
|
functionality: {
|
||||||
|
positionVsParent: 'atEquipment',
|
||||||
|
},
|
||||||
|
asset: {
|
||||||
|
supplier: 'hidrostal',
|
||||||
|
category: 'machine',
|
||||||
|
type: 'pump',
|
||||||
|
model: 'hidrostal-H05K-S03R',
|
||||||
|
unit: 'm3/h',
|
||||||
|
curveUnits: {
|
||||||
|
pressure: 'mbar',
|
||||||
|
flow: 'm3/h',
|
||||||
|
power: 'kW',
|
||||||
|
control: '%',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeStateConfig(overrides = {}) {
|
||||||
|
return {
|
||||||
|
general: {
|
||||||
|
logging: { enabled: false, logLevel: 'error' },
|
||||||
|
},
|
||||||
|
state: {
|
||||||
|
current: 'idle',
|
||||||
|
},
|
||||||
|
movement: {
|
||||||
|
mode: 'staticspeed',
|
||||||
|
speed: 1000,
|
||||||
|
maxSpeed: 1000,
|
||||||
|
interval: 10,
|
||||||
|
},
|
||||||
|
time: {
|
||||||
|
starting: 0,
|
||||||
|
warmingup: 0,
|
||||||
|
stopping: 0,
|
||||||
|
coolingdown: 0,
|
||||||
|
},
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeChildMeasurement({ id = 'child-1', name = 'PT-1', positionVsParent = 'downstream', type = 'pressure', unit = 'mbar' } = {}) {
|
||||||
|
const measurements = new MeasurementContainer({
|
||||||
|
autoConvert: true,
|
||||||
|
defaultUnits: {
|
||||||
|
pressure: 'mbar',
|
||||||
|
flow: 'm3/h',
|
||||||
|
temperature: 'C',
|
||||||
|
power: 'kW',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
config: {
|
||||||
|
general: { id, name },
|
||||||
|
functionality: { positionVsParent },
|
||||||
|
asset: { type, unit },
|
||||||
|
},
|
||||||
|
measurements,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeNodeStub() {
|
||||||
|
const handlers = {};
|
||||||
|
const sent = [];
|
||||||
|
const statuses = [];
|
||||||
|
const errors = [];
|
||||||
|
const warns = [];
|
||||||
|
return {
|
||||||
|
id: 'node-1',
|
||||||
|
source: null,
|
||||||
|
send(msg) { sent.push(msg); },
|
||||||
|
status(s) { statuses.push(s); },
|
||||||
|
error(e) { errors.push(e); },
|
||||||
|
warn(w) { warns.push(w); },
|
||||||
|
on(event, cb) { handlers[event] = cb; },
|
||||||
|
_handlers: handlers,
|
||||||
|
_sent: sent,
|
||||||
|
_statuses: statuses,
|
||||||
|
_errors: errors,
|
||||||
|
_warns: warns,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeREDStub(nodeMap = {}) {
|
||||||
|
return {
|
||||||
|
nodes: {
|
||||||
|
getNode(id) {
|
||||||
|
return nodeMap[id] || null;
|
||||||
|
},
|
||||||
|
createNode() {},
|
||||||
|
registerType() {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
makeMachineConfig,
|
||||||
|
makeStateConfig,
|
||||||
|
makeChildMeasurement,
|
||||||
|
makeNodeStub,
|
||||||
|
makeREDStub,
|
||||||
|
};
|
||||||
107
test/integration/basic-flow-dashboard.integration.test.js
Normal file
107
test/integration/basic-flow-dashboard.integration.test.js
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
const test = require('node:test');
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const path = require('node:path');
|
||||||
|
|
||||||
|
function loadBasicFlow() {
|
||||||
|
const flowPath = path.join(__dirname, '../../examples/basic.flow.json');
|
||||||
|
return JSON.parse(fs.readFileSync(flowPath, 'utf8'));
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeContextStub() {
|
||||||
|
const store = {};
|
||||||
|
return {
|
||||||
|
get(key) {
|
||||||
|
return store[key];
|
||||||
|
},
|
||||||
|
set(key, value) {
|
||||||
|
store[key] = value;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
test('basic flow parser routes predicted_power to output index 2 with numeric payload', () => {
|
||||||
|
const flow = loadBasicFlow();
|
||||||
|
const parser = flow.find((n) => n.id === 'rm_parse_output');
|
||||||
|
assert.ok(parser, 'rm_parse_output node should exist');
|
||||||
|
assert.equal(parser.outputs, 11);
|
||||||
|
|
||||||
|
const func = new Function('msg', 'context', 'node', parser.func);
|
||||||
|
const context = makeContextStub();
|
||||||
|
const node = { send() {} };
|
||||||
|
|
||||||
|
const msg = {
|
||||||
|
payload: {
|
||||||
|
'flow.predicted.downstream.default': 220,
|
||||||
|
'power.predicted.atequipment.default': 50,
|
||||||
|
ctrl: 40,
|
||||||
|
NCogPercent: 72,
|
||||||
|
state: 'operational',
|
||||||
|
mode: 'virtualControl',
|
||||||
|
runtime: 10.2,
|
||||||
|
moveTimeleft: 0,
|
||||||
|
maintenanceTime: 150.5,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const out = func(msg, context, node);
|
||||||
|
assert.ok(Array.isArray(out));
|
||||||
|
assert.equal(out.length, 11);
|
||||||
|
assert.equal(out[1].topic, 'predicted_power');
|
||||||
|
assert.equal(typeof out[1].payload, 'number');
|
||||||
|
assert.ok(Number.isFinite(out[1].payload));
|
||||||
|
assert.equal(out[1].payload, 50);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('basic flow parser output index wiring matches chart nodes', () => {
|
||||||
|
const flow = loadBasicFlow();
|
||||||
|
const parser = flow.find((n) => n.id === 'rm_parse_output');
|
||||||
|
const powerChart = flow.find((n) => n.id === 'rm_chart_power');
|
||||||
|
assert.ok(parser, 'rm_parse_output node should exist');
|
||||||
|
assert.ok(powerChart, 'rm_chart_power node should exist');
|
||||||
|
|
||||||
|
assert.equal(parser.wires[1][0], 'rm_chart_power');
|
||||||
|
assert.equal(powerChart.type, 'ui-chart');
|
||||||
|
assert.equal(powerChart.chartType, 'line');
|
||||||
|
assert.equal(powerChart.xAxisType, 'time');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('basic flow parser routes pressure series to explicit pressure charts', () => {
|
||||||
|
const flow = loadBasicFlow();
|
||||||
|
const parser = flow.find((n) => n.id === 'rm_parse_output');
|
||||||
|
const upChart = flow.find((n) => n.id === 'rm_chart_pressure_up');
|
||||||
|
const downChart = flow.find((n) => n.id === 'rm_chart_pressure_down');
|
||||||
|
const deltaChart = flow.find((n) => n.id === 'rm_chart_pressure_delta');
|
||||||
|
|
||||||
|
assert.ok(parser, 'rm_parse_output node should exist');
|
||||||
|
assert.ok(upChart, 'rm_chart_pressure_up node should exist');
|
||||||
|
assert.ok(downChart, 'rm_chart_pressure_down node should exist');
|
||||||
|
assert.ok(deltaChart, 'rm_chart_pressure_delta node should exist');
|
||||||
|
|
||||||
|
assert.equal(parser.wires[5][0], 'rm_chart_pressure_up');
|
||||||
|
assert.equal(parser.wires[6][0], 'rm_chart_pressure_down');
|
||||||
|
assert.equal(parser.wires[7][0], 'rm_chart_pressure_delta');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('basic flow parser suppresses pressure chart messages when pressure inputs are incomplete', () => {
|
||||||
|
const flow = loadBasicFlow();
|
||||||
|
const parser = flow.find((n) => n.id === 'rm_parse_output');
|
||||||
|
assert.ok(parser, 'rm_parse_output node should exist');
|
||||||
|
|
||||||
|
const func = new Function('msg', 'context', 'node', parser.func);
|
||||||
|
const context = makeContextStub();
|
||||||
|
const node = { send() {} };
|
||||||
|
|
||||||
|
// Only upstream present: downstream/delta chart outputs should be null
|
||||||
|
let out = func({ payload: { 'pressure.measured.upstream.default': 950 } }, context, node);
|
||||||
|
assert.equal(out[5]?.topic, 'pressure_upstream');
|
||||||
|
assert.equal(out[6], null);
|
||||||
|
assert.equal(out[7], null);
|
||||||
|
|
||||||
|
// Once downstream arrives, delta should be emitted as finite numeric payload
|
||||||
|
out = func({ payload: { 'pressure.measured.downstream.default': 1200 } }, context, node);
|
||||||
|
assert.equal(out[6]?.topic, 'pressure_downstream');
|
||||||
|
assert.equal(out[7]?.topic, 'pressure_delta');
|
||||||
|
assert.equal(typeof out[7].payload, 'number');
|
||||||
|
assert.ok(Number.isFinite(out[7].payload));
|
||||||
|
});
|
||||||
54
test/integration/coolprop.integration.test.js
Normal file
54
test/integration/coolprop.integration.test.js
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
const test = require('node:test');
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
|
||||||
|
const Machine = require('../../src/specificClass');
|
||||||
|
const { makeMachineConfig, makeStateConfig } = require('../helpers/factories');
|
||||||
|
|
||||||
|
test('calcEfficiency runs through coolprop path without mocks', () => {
|
||||||
|
const machine = new Machine(makeMachineConfig(), makeStateConfig({ state: { current: 'operational' } }));
|
||||||
|
|
||||||
|
machine.measurements.type('pressure').variant('measured').position('downstream').value(1200, Date.now(), 'mbar');
|
||||||
|
machine.measurements.type('pressure').variant('measured').position('upstream').value(800, Date.now(), 'mbar');
|
||||||
|
machine.measurements.type('flow').variant('predicted').position('atEquipment').value(120, Date.now(), 'm3/h');
|
||||||
|
machine.measurements.type('power').variant('predicted').position('atEquipment').value(12, Date.now(), 'kW');
|
||||||
|
|
||||||
|
assert.doesNotThrow(() => {
|
||||||
|
machine.calcEfficiency(12, 120, 'predicted');
|
||||||
|
});
|
||||||
|
|
||||||
|
const eff = machine.measurements.type('efficiency').variant('predicted').position('atEquipment').getCurrentValue();
|
||||||
|
assert.equal(typeof eff, 'number');
|
||||||
|
assert.ok(eff > 0);
|
||||||
|
|
||||||
|
const pressureDiffPa = (1200 - 800) * 100; // mbar -> Pa
|
||||||
|
const flowM3s = 120 / 3600; // m3/h -> m3/s
|
||||||
|
const expectedHydraulicPower = pressureDiffPa * flowM3s;
|
||||||
|
const expectedHydraulicEfficiency = expectedHydraulicPower / 12000; // 12kW -> W
|
||||||
|
|
||||||
|
const hydraulicPower = machine.measurements.type('hydraulicPower').variant('predicted').position('atEquipment').getCurrentValue('W');
|
||||||
|
const hydraulicEfficiency = machine.measurements.type('nHydraulicEfficiency').variant('predicted').position('atEquipment').getCurrentValue();
|
||||||
|
const head = machine.measurements.type('pumpHead').variant('predicted').position('atEquipment').getCurrentValue('m');
|
||||||
|
|
||||||
|
assert.ok(Number.isFinite(hydraulicPower));
|
||||||
|
assert.ok(Number.isFinite(hydraulicEfficiency));
|
||||||
|
assert.ok(Number.isFinite(head));
|
||||||
|
assert.ok(Math.abs(hydraulicPower - expectedHydraulicPower) < 1);
|
||||||
|
assert.ok(Math.abs(hydraulicEfficiency - expectedHydraulicEfficiency) < 0.01);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('predictions use initialized medium pressure and not the minimum-pressure fallback', () => {
|
||||||
|
const machine = new Machine(makeMachineConfig(), makeStateConfig({ state: { current: 'operational' } }));
|
||||||
|
|
||||||
|
const mediumUpstreamMbar = 700;
|
||||||
|
const mediumDownstreamMbar = 1100;
|
||||||
|
machine.updateMeasuredPressure(mediumUpstreamMbar, 'upstream', { timestamp: Date.now(), unit: 'mbar', childName: 'test-pt-up' });
|
||||||
|
machine.updateMeasuredPressure(mediumDownstreamMbar, 'downstream', { timestamp: Date.now(), unit: 'mbar', childName: 'test-pt-down' });
|
||||||
|
|
||||||
|
const pressureStatus = machine.getPressureInitializationStatus();
|
||||||
|
assert.equal(pressureStatus.initialized, true);
|
||||||
|
assert.equal(pressureStatus.hasDifferential, true);
|
||||||
|
|
||||||
|
const expectedDiff = (mediumDownstreamMbar - mediumUpstreamMbar) * 100; // mbar -> Pa canonical
|
||||||
|
assert.equal(Math.round(machine.predictFlow.fDimension), expectedDiff);
|
||||||
|
assert.ok(machine.predictFlow.fDimension > 0);
|
||||||
|
});
|
||||||
75
test/integration/prediction-health.integration.test.js
Normal file
75
test/integration/prediction-health.integration.test.js
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
const test = require('node:test');
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
|
||||||
|
const Machine = require('../../src/specificClass');
|
||||||
|
const { makeMachineConfig, makeStateConfig } = require('../helpers/factories');
|
||||||
|
|
||||||
|
test('flow drift is assessed with NRMSE and exposed in output', () => {
|
||||||
|
const machine = new Machine(makeMachineConfig(), makeStateConfig({ state: { current: 'operational' } }));
|
||||||
|
|
||||||
|
machine.updateMeasuredPressure(700, 'upstream', { timestamp: Date.now(), unit: 'mbar', childName: 'pt-up' });
|
||||||
|
machine.updateMeasuredPressure(1100, 'downstream', { timestamp: Date.now(), unit: 'mbar', childName: 'pt-down' });
|
||||||
|
machine.updatePosition();
|
||||||
|
|
||||||
|
const predictedFlow = machine.measurements
|
||||||
|
.type('flow')
|
||||||
|
.variant('predicted')
|
||||||
|
.position('downstream')
|
||||||
|
.getCurrentValue('m3/h');
|
||||||
|
|
||||||
|
for (let i = 0; i < 10; i += 1) {
|
||||||
|
machine.updateMeasuredFlow(predictedFlow * 0.92, 'downstream', {
|
||||||
|
timestamp: Date.now() + i,
|
||||||
|
unit: 'm3/h',
|
||||||
|
childName: 'ft-down',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const output = machine.getOutput();
|
||||||
|
assert.ok(Number.isFinite(output.flowNrmse));
|
||||||
|
assert.equal(typeof output.flowImmediateLevel, 'number');
|
||||||
|
assert.equal(typeof output.flowLongTermLevel, 'number');
|
||||||
|
assert.ok(['high', 'medium', 'low', 'invalid'].includes(output.predictionQuality));
|
||||||
|
assert.ok(Number.isFinite(output.predictionConfidence));
|
||||||
|
assert.equal(output.predictionPressureSource, 'differential');
|
||||||
|
assert.ok(Array.isArray(output.predictionFlags));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('power drift is assessed when measured power is provided', () => {
|
||||||
|
const machine = new Machine(makeMachineConfig(), makeStateConfig({ state: { current: 'operational' } }));
|
||||||
|
|
||||||
|
machine.updateMeasuredPressure(700, 'upstream', { timestamp: Date.now(), unit: 'mbar', childName: 'pt-up' });
|
||||||
|
machine.updateMeasuredPressure(1100, 'downstream', { timestamp: Date.now(), unit: 'mbar', childName: 'pt-down' });
|
||||||
|
machine.updatePosition();
|
||||||
|
|
||||||
|
const predictedPower = machine.measurements
|
||||||
|
.type('power')
|
||||||
|
.variant('predicted')
|
||||||
|
.position('atEquipment')
|
||||||
|
.getCurrentValue('kW');
|
||||||
|
|
||||||
|
for (let i = 0; i < 10; i += 1) {
|
||||||
|
machine.updateMeasuredPower(predictedPower * 1.08, 'atEquipment', {
|
||||||
|
timestamp: Date.now() + i,
|
||||||
|
unit: 'kW',
|
||||||
|
childName: 'power-meter',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const output = machine.getOutput();
|
||||||
|
assert.ok(Number.isFinite(output.powerNrmse));
|
||||||
|
assert.equal(typeof output.powerImmediateLevel, 'number');
|
||||||
|
assert.equal(typeof output.powerLongTermLevel, 'number');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('single-side pressure lowers prediction confidence category', () => {
|
||||||
|
const machine = new Machine(makeMachineConfig(), makeStateConfig({ state: { current: 'operational' } }));
|
||||||
|
machine.updateMeasuredPressure(950, 'downstream', { timestamp: Date.now(), unit: 'mbar', childName: 'pt-down' });
|
||||||
|
|
||||||
|
const output = machine.getOutput();
|
||||||
|
assert.equal(output.predictionPressureSource, 'downstream');
|
||||||
|
assert.ok(output.predictionConfidence < 0.9);
|
||||||
|
assert.equal(output.pressureDriftLevel, 1);
|
||||||
|
assert.ok(Array.isArray(output.predictionFlags));
|
||||||
|
assert.ok(output.predictionFlags.includes('single_side_pressure'));
|
||||||
|
});
|
||||||
84
test/integration/pressure-initialization.integration.test.js
Normal file
84
test/integration/pressure-initialization.integration.test.js
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
const test = require('node:test');
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
|
||||||
|
const Machine = require('../../src/specificClass');
|
||||||
|
const { makeMachineConfig, makeStateConfig, makeChildMeasurement } = require('../helpers/factories');
|
||||||
|
|
||||||
|
test('pressure initialization combinations are handled explicitly', () => {
|
||||||
|
const createMachine = () => new Machine(makeMachineConfig(), makeStateConfig({ state: { current: 'operational' } }));
|
||||||
|
|
||||||
|
// nothing
|
||||||
|
let machine = createMachine();
|
||||||
|
let status = machine.getPressureInitializationStatus();
|
||||||
|
assert.equal(status.initialized, false);
|
||||||
|
assert.equal(status.source, null);
|
||||||
|
const noPressureValue = machine.getMeasuredPressure();
|
||||||
|
assert.equal(noPressureValue, 0);
|
||||||
|
assert.ok(machine.predictFlow.fDimension <= 1);
|
||||||
|
|
||||||
|
// upstream only
|
||||||
|
machine = createMachine();
|
||||||
|
const upstreamOnly = 850;
|
||||||
|
machine.measurements.type('pressure').variant('measured').position('upstream').value(upstreamOnly, Date.now(), 'mbar');
|
||||||
|
status = machine.getPressureInitializationStatus();
|
||||||
|
assert.equal(status.initialized, true);
|
||||||
|
assert.equal(status.hasUpstream, true);
|
||||||
|
assert.equal(status.hasDownstream, false);
|
||||||
|
assert.equal(status.hasDifferential, false);
|
||||||
|
assert.equal(status.source, 'upstream');
|
||||||
|
const upstreamValue = machine.getMeasuredPressure();
|
||||||
|
assert.equal(Math.round(upstreamValue), upstreamOnly * 100);
|
||||||
|
assert.equal(Math.round(machine.predictFlow.fDimension), upstreamOnly * 100);
|
||||||
|
|
||||||
|
// downstream only
|
||||||
|
machine = createMachine();
|
||||||
|
const downstreamOnly = 1150;
|
||||||
|
machine.measurements.type('pressure').variant('measured').position('downstream').value(downstreamOnly, Date.now(), 'mbar');
|
||||||
|
status = machine.getPressureInitializationStatus();
|
||||||
|
assert.equal(status.initialized, true);
|
||||||
|
assert.equal(status.hasUpstream, false);
|
||||||
|
assert.equal(status.hasDownstream, true);
|
||||||
|
assert.equal(status.hasDifferential, false);
|
||||||
|
assert.equal(status.source, 'downstream');
|
||||||
|
const downstreamValue = machine.getMeasuredPressure();
|
||||||
|
assert.equal(Math.round(downstreamValue), downstreamOnly * 100);
|
||||||
|
assert.equal(Math.round(machine.predictFlow.fDimension), downstreamOnly * 100);
|
||||||
|
|
||||||
|
// downstream and upstream
|
||||||
|
machine = createMachine();
|
||||||
|
const upstream = 700;
|
||||||
|
const downstream = 1100;
|
||||||
|
machine.measurements.type('pressure').variant('measured').position('upstream').value(upstream, Date.now(), 'mbar');
|
||||||
|
machine.measurements.type('pressure').variant('measured').position('downstream').value(downstream, Date.now(), 'mbar');
|
||||||
|
status = machine.getPressureInitializationStatus();
|
||||||
|
assert.equal(status.initialized, true);
|
||||||
|
assert.equal(status.hasUpstream, true);
|
||||||
|
assert.equal(status.hasDownstream, true);
|
||||||
|
assert.equal(status.hasDifferential, true);
|
||||||
|
assert.equal(status.source, 'differential');
|
||||||
|
const differentialValue = machine.getMeasuredPressure();
|
||||||
|
assert.equal(Math.round(differentialValue), (downstream - upstream) * 100);
|
||||||
|
assert.equal(Math.round(machine.predictFlow.fDimension), (downstream - upstream) * 100);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('real pressure child data has priority over simulated dashboard pressure', async () => {
|
||||||
|
const machine = new Machine(makeMachineConfig(), makeStateConfig({ state: { current: 'operational' } }));
|
||||||
|
|
||||||
|
machine.updateSimulatedMeasurement('pressure', 'upstream', 900, { unit: 'mbar', timestamp: Date.now() });
|
||||||
|
machine.updateSimulatedMeasurement('pressure', 'downstream', 1200, { unit: 'mbar', timestamp: Date.now() });
|
||||||
|
assert.equal(Math.round(machine.getMeasuredPressure()), 30000);
|
||||||
|
|
||||||
|
const upstreamChild = makeChildMeasurement({ id: 'pt-up-real', name: 'PT Up', positionVsParent: 'upstream', type: 'pressure', unit: 'mbar' });
|
||||||
|
const downstreamChild = makeChildMeasurement({ id: 'pt-down-real', name: 'PT Down', positionVsParent: 'downstream', type: 'pressure', unit: 'mbar' });
|
||||||
|
|
||||||
|
await machine.childRegistrationUtils.registerChild(upstreamChild, 'upstream');
|
||||||
|
await machine.childRegistrationUtils.registerChild(downstreamChild, 'downstream');
|
||||||
|
|
||||||
|
upstreamChild.measurements.type('pressure').variant('measured').position('upstream').value(700, Date.now(), 'mbar');
|
||||||
|
downstreamChild.measurements.type('pressure').variant('measured').position('downstream').value(1300, Date.now(), 'mbar');
|
||||||
|
|
||||||
|
assert.equal(Math.round(machine.getMeasuredPressure()), 60000);
|
||||||
|
const status = machine.getPressureInitializationStatus();
|
||||||
|
assert.equal(status.source, 'differential');
|
||||||
|
assert.equal(status.initialized, true);
|
||||||
|
});
|
||||||
53
test/integration/registration.integration.test.js
Normal file
53
test/integration/registration.integration.test.js
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
const test = require('node:test');
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
|
||||||
|
const Machine = require('../../src/specificClass');
|
||||||
|
const { makeMachineConfig, makeStateConfig, makeChildMeasurement } = require('../helpers/factories');
|
||||||
|
|
||||||
|
test('registerChild listens to measurement events and stores measured pressure', async () => {
|
||||||
|
const machine = new Machine(makeMachineConfig(), makeStateConfig());
|
||||||
|
const child = makeChildMeasurement({ positionVsParent: 'downstream', type: 'pressure', unit: 'mbar' });
|
||||||
|
|
||||||
|
machine.registerChild(child, 'measurement');
|
||||||
|
|
||||||
|
child.measurements
|
||||||
|
.type('pressure')
|
||||||
|
.variant('measured')
|
||||||
|
.position('downstream')
|
||||||
|
.value(123, Date.now(), 'mbar');
|
||||||
|
|
||||||
|
const stored = machine.measurements
|
||||||
|
.type('pressure')
|
||||||
|
.variant('measured')
|
||||||
|
.position('downstream')
|
||||||
|
.getCurrentValue('mbar');
|
||||||
|
|
||||||
|
assert.equal(typeof stored, 'number');
|
||||||
|
assert.equal(Math.round(stored), 123);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('registerChild deduplicates listeners on re-registration', async () => {
|
||||||
|
const machine = new Machine(makeMachineConfig(), makeStateConfig());
|
||||||
|
const child = makeChildMeasurement({ id: 'pt-dup', positionVsParent: 'downstream', type: 'pressure', unit: 'mbar' });
|
||||||
|
const eventName = 'pressure.measured.downstream';
|
||||||
|
|
||||||
|
let handlerCalls = 0;
|
||||||
|
const originalUpdatePressure = machine.updateMeasuredPressure.bind(machine);
|
||||||
|
machine.updateMeasuredPressure = (...args) => {
|
||||||
|
handlerCalls += 1;
|
||||||
|
return originalUpdatePressure(...args);
|
||||||
|
};
|
||||||
|
|
||||||
|
machine.registerChild(child, 'measurement');
|
||||||
|
machine.registerChild(child, 'measurement');
|
||||||
|
|
||||||
|
assert.equal(child.measurements.emitter.listenerCount(eventName), 1);
|
||||||
|
|
||||||
|
child.measurements
|
||||||
|
.type('pressure')
|
||||||
|
.variant('measured')
|
||||||
|
.position('downstream')
|
||||||
|
.value(321, Date.now(), 'mbar');
|
||||||
|
|
||||||
|
assert.equal(handlerCalls, 1);
|
||||||
|
});
|
||||||
24
test/integration/sequences.integration.test.js
Normal file
24
test/integration/sequences.integration.test.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
const test = require('node:test');
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
|
||||||
|
const Machine = require('../../src/specificClass');
|
||||||
|
const { makeMachineConfig, makeStateConfig } = require('../helpers/factories');
|
||||||
|
|
||||||
|
test('execSequence startup reaches operational with zero transition times', async () => {
|
||||||
|
const machine = new Machine(makeMachineConfig(), makeStateConfig());
|
||||||
|
|
||||||
|
await machine.handleInput('parent', 'execSequence', 'startup');
|
||||||
|
|
||||||
|
assert.equal(machine.state.getCurrentState(), 'operational');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('execMovement constrains controller position to safe bounds in operational state', async () => {
|
||||||
|
const machine = new Machine(makeMachineConfig(), makeStateConfig({ state: { current: 'operational' } }));
|
||||||
|
const { max } = machine._resolveSetpointBounds();
|
||||||
|
|
||||||
|
await machine.handleInput('parent', 'execMovement', 10);
|
||||||
|
|
||||||
|
const pos = machine.state.getCurrentPosition();
|
||||||
|
assert.ok(pos <= max);
|
||||||
|
assert.equal(pos, max);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user