UNPKG

@opcua/for-node-red

Version:

The Node-RED node to communicate via OPC UA, powered NodeOPCUA and developed by Sterfive's team

1,705 lines 109 kB
[ { "id": "c4495043459c465d", "type": "subflow", "name": "gather & repeat", "info": "", "category": "", "in": [ { "x": 60, "y": 80, "wires": [ { "id": "e97157a71ff3fc6b" } ] } ], "out": [ { "x": 380, "y": 80, "wires": [ { "id": "e97157a71ff3fc6b", "port": 0 } ] } ], "env": [], "meta": {}, "color": "#DDAA99" }, { "id": "e97157a71ff3fc6b", "type": "function", "z": "c4495043459c465d", "name": "gather & repeat", "func": "/**\n * This function node receives a monitored DataValue from OPC UA.\n * - msg.payload: Contains the value to be parsed into a float.\n * - msg.sourceTimestamp: Contains the value's timestamp in UTC ISO string format.\n *\n * It outputs messages to the first output with the following payload structure:\n * {\n * timestamp: new Date(msg.sourceTimestamp),\n * value: String(parseFloat(msg.payload))\n * }\n *\n * OPC UA only sends data when a value changes. To maintain a steady data stream, this node\n * repeats the last received value every second. The first repeated message is sent 1.5 seconds\n * after the initial message, with timestamps incrementing by the 1-second interval.\n *\n * Key constraints:\n * - Timestamps must always progress forward.\n * - Timing is based solely on OPC UA timestamps to avoid clock skew issues.\n * - Maintain only a single active timer to prevent duplicate messages.\n * - Timers are cleared properly upon node redeploy.\n */\n\nconst INTERVAL_MS = 1000; // 1-second interval for repeats\nconst INITIAL_DELAY_MS = 1500; // 1.5-second initial delay\n\n// Initialize context variables\nlet timer = context.get('timer');\nlet lastSentTimestamp = context.get('lastSentTimestamp') || 0;\nlet lastValue = context.get('lastValue');\nlet nextExpectedTimestamp = context.get('nextExpectedTimestamp') || 0;\n\n// Cleanup on node redeploy\nif (!context.get('initialized')) {\n node.on('close', function() {\n if (timer) {\n clearTimeout(timer);\n context.set('timer', null);\n }\n });\n context.set('initialized', true);\n}\n\nfunction sendAndSchedule(value, sourceTime) {\n // Send the message immediately\n node.send({\n payload: {\n timestamp: new Date(sourceTime),\n value: value\n }\n });\n\n // Update context\n context.set('lastSentTimestamp', sourceTime);\n context.set('lastValue', value);\n\n // Calculate the next expected timestamp\n const nextTs = sourceTime + INITIAL_DELAY_MS + INTERVAL_MS;\n context.set('nextExpectedTimestamp', nextTs);\n\n // Schedule the next repetition\n const delayUntilNextRepeat = INITIAL_DELAY_MS;\n timer = setTimeout(() => {\n sendRepeatedValue(value, nextTs, INTERVAL_MS);\n }, delayUntilNextRepeat);\n context.set('timer', timer);\n}\n\nfunction sendRepeatedValue(value, nextTs, interval) {\n // Send the repeated message\n node.send({\n payload: {\n timestamp: new Date(nextTs),\n value: value\n }\n });\n\n // Schedule the next repetition\n const nextNextTs = nextTs + interval;\n context.set('nextExpectedTimestamp', nextNextTs);\n\n timer = setTimeout(() => {\n sendRepeatedValue(value, nextNextTs, interval);\n }, interval);\n context.set('timer', timer);\n}\n\nif (msg.payload !== undefined && msg.sourceTimestamp) {\n // Clear any existing timer\n if (timer) {\n clearTimeout(timer);\n context.set('timer', null);\n }\n\n try {\n let sourceTime = new Date(msg.sourceTimestamp).getTime();\n const value = \"\" + parseFloat(msg.payload);\n\n // Ensure timestamps always go forward\n if (lastSentTimestamp && sourceTime <= lastSentTimestamp) {\n sourceTime = lastSentTimestamp + 1; // Force it to be strictly greater\n }\n\n // Send immediately and schedule the first repeat\n sendAndSchedule(value, sourceTime);\n } catch (e) {\n node.error(\"Error processing message: \" + e.message, msg);\n }\n}", "outputs": 1, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 220, "y": 80, "wires": [ [] ] }, { "id": "8914237e0d8e46c7", "type": "tab", "label": "Advanced Coffee Machine Example", "disabled": false, "info": "", "env": [] }, { "id": "a872632f0795dffe", "type": "group", "z": "8914237e0d8e46c7", "style": { "stroke": "#999999", "stroke-opacity": "1", "fill": "none", "fill-opacity": "1", "label": true, "label-position": "nw", "color": "#a4a4a4" }, "nodes": [ "5163f0e3c15147e2", "9ce44b437e2ed4eb", "17f15759ea0b4678", "ae2a5d89ca13248d", "44eeed6c80419b5f", "7b496ba60d331069", "1c3d6230a57c852c", "c172f527ba188866", "ae3fe0588bcec600", "83de8924a1f6f2a5", "807487cd9637d0e1", "e181ef3ed9e6dc40", "6c7658b34fce0ac3" ], "x": 1114, "y": 119, "w": 432, "h": 482 }, { "id": "23e92d93ed23febc", "type": "group", "z": "8914237e0d8e46c7", "name": "Connection status", "style": { "stroke": "#ffC000", "label": true }, "nodes": [ "dc5e73eb1991eed3", "36b2628c69b87d42", "f79e321ce9b22631", "46d99d7986a03b57", "e975667424fe78b7", "dcf7ab764b7d2921" ], "x": 194, "y": 939, "w": 812, "h": 242 }, { "id": "b07d4a44ddb86adf", "type": "group", "z": "8914237e0d8e46c7", "name": "Heater", "style": { "label": true }, "nodes": [ "3e9be0f18116b2c2", "2b573431a25e870f", "68e0c3653588a07d", "d3b1d8b5c05c2b22", "07bde3d3f85f2a40", "c369c3262a3b57fa", "f1f77b6afa4257f0", "20844959a8ea715c" ], "x": 194, "y": 699, "w": 812, "h": 222 }, { "id": "62d148a7e9f5c520", "type": "group", "z": "8914237e0d8e46c7", "name": "Current State", "style": { "label": true }, "nodes": [ "c9717ac11b53672e", "3a8c7eb7dc515e34", "798535950531337a" ], "x": 1054, "y": 699, "w": 632, "h": 82 }, { "id": "11f19caa7b9760b7", "type": "group", "z": "8914237e0d8e46c7", "name": "Pump", "style": { "label": true }, "nodes": [ "f3ba9efdc12e30ce", "24ab918428d73e5f", "b8ba5e020aa48e2a", "7619f93616149cdc" ], "x": 1054, "y": 799, "w": 632, "h": 122 }, { "id": "f250c8ad248b028d", "type": "group", "z": "8914237e0d8e46c7", "name": "Process Variables", "style": { "fill": "#ffffbf", "label": true }, "nodes": [ "517edf10f355e464", "d22591b8e78ebe7d", "29542b83f026fb7b", "684238fb58a91415" ], "x": 188, "y": 53, "w": 724, "h": 634 }, { "id": "ec1ab7ca9e060d7f", "type": "group", "z": "8914237e0d8e46c7", "style": { "stroke": "#999999", "stroke-opacity": "1", "fill": "none", "fill-opacity": "1", "label": true, "label-position": "nw", "color": "#a4a4a4" }, "nodes": [ "4cecc03d1a64c091", "f6b1e7b0df0fa288", "0c098347de28a15d", "57622c7f24dacef7", "237e980896f1a684", "e3c1066f3260a1cb", "0597a0ffcdab77cd", "6008fa16cda462d5", "1fca2f6f28cc4828", "2857eb76526322a1", "ea1957e0718f21fd", "03aa8523b17003d7" ], "x": 1054, "y": 939, "w": 812, "h": 242 }, { "id": "230bb82688833807", "type": "group", "z": "8914237e0d8e46c7", "name": "Pooling Read", "style": { "label": true }, "nodes": [ "d21be93e03b60165", "1832b372c56dc715", "5f6eeb0fc5228a66", "5e50f3a2da48a03b", "fe845d4c6aa473d0", "50c33de026727ab2", "99b27a72cfb58370", "829ce0410d7c3b05", "1bea16ead1291025" ], "x": 194, "y": 1419, "w": 1172, "h": 162 }, { "id": "54277e0eb0f4eddd", "type": "group", "z": "8914237e0d8e46c7", "name": "Display NAMUR Symbol", "style": { "label": true }, "nodes": [ "44225cb3561717c6", "61237053c6df429d", "2456ee93776095bc" ], "x": 194, "y": 1599, "w": 712, "h": 82 }, { "id": "665736b8cbeb246d", "type": "group", "z": "8914237e0d8e46c7", "name": "Synopsis", "style": { "stroke": "#ffbfbf", "fill": "#e3f3d3", "label": true }, "nodes": [ "2c7b916b70107153", "eeaa86a8240214df", "42fea19e7e56afbc", "072413d285aeb40e", "f64d872549983d6f", "143a262a2cbfe451", "a1492e996fe8302a" ], "x": 194, "y": 1719, "w": 1012, "h": 162 }, { "id": "a9f1eb481f74dde3", "type": "group", "z": "8914237e0d8e46c7", "name": "Collect data to CSV", "style": { "stroke": "#3f93cf", "fill": "#bfdbef", "label": true }, "nodes": [ "b577d7342c008353", "529de43bbcc8ddd4", "73a7a87a437b298c", "239601af1e96d917", "ccd602507a4abc68", "b694d72cbaef5d85", "af5560e53daca7e6", "96aded34d2e12af9", "14863b828843ebb1", "d95b03beeb91e93e" ], "x": 204, "y": 1219, "w": 1122, "h": 162 }, { "id": "517edf10f355e464", "type": "group", "z": "8914237e0d8e46c7", "g": "f250c8ad248b028d", "name": "Water Tank", "style": { "label": true, "fill": "#bfdbef" }, "nodes": [ "5ddf1b5952313443", "a66ec0e0a1360f34", "9122c1157cbe0b5e", "d2a8ee6868d23307", "0f373262c51c849b", "b4b79354c06cc405" ], "x": 214, "y": 79, "w": 672, "h": 162 }, { "id": "d22591b8e78ebe7d", "type": "group", "z": "8914237e0d8e46c7", "g": "f250c8ad248b028d", "name": "Milk Tank", "style": { "label": true }, "nodes": [ "49367ab61a7d3443", "bbfc30b4f5fe5cdd", "ab80657e6899a890", "c5df8a8624445a8a", "da3f9ba191429d15" ], "x": 214, "y": 259, "w": 652, "h": 122 }, { "id": "29542b83f026fb7b", "type": "group", "z": "8914237e0d8e46c7", "g": "f250c8ad248b028d", "name": "Coffee Beam Tank", "style": { "label": true }, "nodes": [ "0ab25a3cc9a2fb4c", "f64ae2163a90f520", "ffdc07a2a8644ee7", "4d754c69f2e0b376", "77fa0ee8e20cbe27" ], "x": 214, "y": 399, "w": 672, "h": 122 }, { "id": "684238fb58a91415", "type": "group", "z": "8914237e0d8e46c7", "g": "f250c8ad248b028d", "name": "Boiler", "style": { "label": true }, "nodes": [ "2cfd5287e52e0fb0", "400b6980b1a2cd45", "4f1eb368f3fb45c1", "528f9a8a184a078a", "f4abb62d50e829d9", "3287809947131ac3", "bc32ee4142fc26ab", "61068469fd79b1a7" ], "x": 214, "y": 539, "w": 632, "h": 122 }, { "id": "5ddf1b5952313443", "type": "OpcUa-Client2-Monitor", "z": "8914237e0d8e46c7", "g": "517edf10f355e464", "inputs": 0, "output": 2, "name": "WaterTankLevel", "endpoint": "9593045b235c321c", "subscription": "s0", "startImmediately": true, "nodeId": "/di:DeviceSet/ns1:CoffeeMachineA.kitchen:Parameters.sterfive:WaterTankLevel", "samplingInterval": "500", "discardOldest": true, "queueSize": 10, "x": 320, "y": 160, "wires": [ [ "d2a8ee6868d23307", "b4b79354c06cc405" ] ] }, { "id": "5163f0e3c15147e2", "type": "OpcUa-Client2-Call", "z": "8914237e0d8e46c7", "g": "a872632f0795dffe", "inputs": 1, "output": 4, "name": "Make Coffee", "endpoint": "9593045b235c321c", "action": "read", "methodId": "/di:DeviceSet/ns1:CoffeeMachineA.di:MethodSet.sterfive:MakeCoffee", "objectId": "/di:DeviceSet/ns1:CoffeeMachineA", "x": 1450, "y": 220, "wires": [ [] ] }, { "id": "9ce44b437e2ed4eb", "type": "inject", "z": "8914237e0d8e46c7", "g": "a872632f0795dffe", "name": "Mocha", "props": [ { "p": "payload" } ], "repeat": "", "crontab": "", "once": false, "onceDelay": 0.1, "topic": "", "payload": "{\"RecipeName\": \"Mocha\"}", "payloadType": "json", "x": 1230, "y": 220, "wires": [ [ "5163f0e3c15147e2" ] ] }, { "id": "17f15759ea0b4678", "type": "OpcUa-Client2-Call", "z": "8914237e0d8e46c7", "g": "a872632f0795dffe", "inputs": 1, "output": 4, "name": "FillTank", "endpoint": "9593045b235c321c", "action": "read", "methodId": "/di:DeviceSet/ns1:CoffeeMachineA.di:MethodSet.sterfive:FillTank", "objectId": "/di:DeviceSet/ns1:CoffeeMachineA", "x": 1450, "y": 320, "wires": [ [] ] }, { "id": "ae2a5d89ca13248d", "type": "inject", "z": "8914237e0d8e46c7", "g": "a872632f0795dffe", "name": "", "props": [], "repeat": "", "crontab": "", "once": false, "onceDelay": 0.1, "topic": "", "x": 1230, "y": 320, "wires": [ [ "17f15759ea0b4678" ] ] }, { "id": "2cfd5287e52e0fb0", "type": "OpcUa-Client2-Monitor", "z": "8914237e0d8e46c7", "g": "684238fb58a91415", "inputs": 0, "output": 2, "name": "BoilerTempWater", "endpoint": "9593045b235c321c", "subscription": "s2", "startImmediately": true, "nodeId": "/di:DeviceSet/ns1:CoffeeMachineA.kitchen:Parameters.kitchen:BoilerTempWater", "samplingInterval": "100", "discardOldest": true, "queueSize": 10, "x": 320, "y": 580, "wires": [ [ "528f9a8a184a078a" ] ] }, { "id": "2c7b916b70107153", "type": "OpcUa-Client2-Explore", "z": "8914237e0d8e46c7", "g": "665736b8cbeb246d", "inputs": 1, "output": 4, "name": "", "endpoint": "9593045b235c321c", "nodeId": "/di:DeviceSet/ns1:CoffeeMachineA.kitchen:Parameters", "outputType": "AliasedBrowsePath", "excludeEmpty": false, "followOrganizes": true, "x": 460, "y": 1800, "wires": [ [ "eeaa86a8240214df", "a1492e996fe8302a" ] ] }, { "id": "eeaa86a8240214df", "type": "debug", "z": "8914237e0d8e46c7", "g": "665736b8cbeb246d", "name": "debug 8", "active": false, "tosidebar": true, "console": false, "tostatus": false, "complete": "false", "statusVal": "", "statusType": "auto", "x": 640, "y": 1760, "wires": [] }, { "id": "42fea19e7e56afbc", "type": "inject", "z": "8914237e0d8e46c7", "g": "665736b8cbeb246d", "name": "AutoStart", "props": [], "repeat": "", "crontab": "", "once": true, "onceDelay": "1", "topic": "", "x": 300, "y": 1800, "wires": [ [ "2c7b916b70107153" ] ] }, { "id": "072413d285aeb40e", "type": "OpcUa-Client2-Monitor", "z": "8914237e0d8e46c7", "g": "665736b8cbeb246d", "inputs": 1, "output": 2, "name": "", "endpoint": "9593045b235c321c", "subscription": "s0", "startImmediately": false, "nodeId": "", "samplingInterval": 1000, "discardOldest": true, "queueSize": 10, "x": 820, "y": 1800, "wires": [ [ "f64d872549983d6f", "143a262a2cbfe451" ] ] }, { "id": "f64d872549983d6f", "type": "debug", "z": "8914237e0d8e46c7", "g": "665736b8cbeb246d", "name": "debug 11", "active": false, "tosidebar": true, "console": false, "tostatus": false, "complete": "payload", "targetType": "msg", "statusVal": "", "statusType": "auto", "x": 1020, "y": 1760, "wires": [] }, { "id": "49367ab61a7d3443", "type": "OpcUa-Client2-Monitor", "z": "8914237e0d8e46c7", "g": "d22591b8e78ebe7d", "inputs": 0, "output": 2, "name": "Milk Tank Level", "endpoint": "9593045b235c321c", "subscription": "s0", "startImmediately": true, "nodeId": "/di:DeviceSet/ns1:CoffeeMachineA.kitchen:Parameters.sterfive:MilkTankLevel", "samplingInterval": "10000", "discardOldest": true, "queueSize": 10, "x": 320, "y": 300, "wires": [ [ "c5df8a8624445a8a", "da3f9ba191429d15" ] ] }, { "id": "0ab25a3cc9a2fb4c", "type": "OpcUa-Client2-Monitor", "z": "8914237e0d8e46c7", "g": "29542b83f026fb7b", "inputs": 0, "output": 2, "name": "Coffe Beam Level", "endpoint": "9593045b235c321c", "subscription": "s0", "startImmediately": true, "nodeId": "/di:DeviceSet/ns1:CoffeeMachineA.kitchen:Parameters.sterfive:CoffeeBeanLevel", "samplingInterval": "10000", "discardOldest": true, "queueSize": 10, "x": 330, "y": 440, "wires": [ [ "4d754c69f2e0b376", "77fa0ee8e20cbe27" ] ] }, { "id": "d2a8ee6868d23307", "type": "function", "z": "8914237e0d8e46c7", "g": "517edf10f355e464", "name": "round", "func": "msg.payload = Math.ceil(msg.payload*10)/10\nreturn msg;", "outputs": 1, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 530, "y": 160, "wires": [ [ "9122c1157cbe0b5e", "0f373262c51c849b" ] ] }, { "id": "528f9a8a184a078a", "type": "function", "z": "8914237e0d8e46c7", "g": "684238fb58a91415", "name": "round", "func": "msg.payload = Math.ceil(msg.payload*100)/100\nreturn msg;", "outputs": 1, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 530, "y": 580, "wires": [ [ "400b6980b1a2cd45" ] ] }, { "id": "f4abb62d50e829d9", "type": "function", "z": "8914237e0d8e46c7", "g": "684238fb58a91415", "name": "round", "func": "msg.payload = Math.ceil(msg.payload*100)/100\nreturn msg;", "outputs": 1, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 530, "y": 580, "wires": [ [] ] }, { "id": "4d754c69f2e0b376", "type": "function", "z": "8914237e0d8e46c7", "g": "29542b83f026fb7b", "name": "round", "func": "msg.payload = Math.ceil(msg.payload*1)/1;\nreturn msg;", "outputs": 1, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 530, "y": 440, "wires": [ [ "f64ae2163a90f520" ] ] }, { "id": "c5df8a8624445a8a", "type": "function", "z": "8914237e0d8e46c7", "g": "d22591b8e78ebe7d", "name": "round", "func": "msg.payload = Math.ceil(msg.payload*1)/1\nreturn msg;", "outputs": 1, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 530, "y": 300, "wires": [ [ "bbfc30b4f5fe5cdd" ] ] }, { "id": "3287809947131ac3", "type": "OpcUa-Client2-Monitor", "z": "8914237e0d8e46c7", "g": "684238fb58a91415", "inputs": 0, "output": 2, "name": "BoilerTempWater", "endpoint": "9593045b235c321c", "subscription": "s2", "startImmediately": true, "nodeId": "/di:DeviceSet/ns1:CoffeeMachineA.kitchen:Parameters.kitchen:BoilerTempWater", "samplingInterval": "100", "discardOldest": true, "queueSize": 10, "x": 320, "y": 580, "wires": [ [] ] }, { "id": "bc32ee4142fc26ab", "type": "OpcUa-Client2-Monitor", "z": "8914237e0d8e46c7", "g": "684238fb58a91415", "inputs": 0, "output": 2, "name": "BoilerTempWater", "endpoint": "9593045b235c321c", "subscription": "s0", "startImmediately": true, "nodeId": "/di:DeviceSet/ns1:CoffeeMachineA.kitchen:Parameters.kitchen:BoilerTempWater", "samplingInterval": "10000", "discardOldest": true, "queueSize": 10, "x": 320, "y": 580, "wires": [ [ "61068469fd79b1a7" ] ] }, { "id": "f3ba9efdc12e30ce", "type": "OpcUa-Client2-Monitor", "z": "8914237e0d8e46c7", "g": "11f19caa7b9760b7", "inputs": 0, "output": 2, "name": "PumpStatus", "endpoint": "9593045b235c321c", "subscription": "s2", "startImmediately": true, "nodeId": "/2:DeviceSet/1:CoffeeMachineA.7:Parameters.17:PumpStatus", "samplingInterval": "1000", "discardOldest": true, "queueSize": 10, "x": 1150, "y": 840, "wires": [ [ "b8ba5e020aa48e2a", "7619f93616149cdc" ] ] }, { "id": "b8ba5e020aa48e2a", "type": "debug", "z": "8914237e0d8e46c7", "g": "11f19caa7b9760b7", "name": "debug 7", "active": true, "tosidebar": true, "console": false, "tostatus": false, "complete": "true", "targetType": "full", "statusVal": "", "statusType": "auto", "x": 1380, "y": 880, "wires": [] }, { "id": "3e9be0f18116b2c2", "type": "OpcUa-Client2-Monitor", "z": "8914237e0d8e46c7", "g": "b07d4a44ddb86adf", "inputs": 0, "output": 2, "name": "Heater Status", "endpoint": "9593045b235c321c", "subscription": "s2", "startImmediately": true, "nodeId": "/di:DeviceSet/ns1:CoffeeMachineA.kitchen:Parameters.sterfive:HeaterStatus", "samplingInterval": "1000", "discardOldest": true, "queueSize": 10, "x": 290, "y": 780, "wires": [ [ "2b573431a25e870f", "20844959a8ea715c" ] ] }, { "id": "2b573431a25e870f", "type": "debug", "z": "8914237e0d8e46c7", "g": "b07d4a44ddb86adf", "name": "debug 10", "active": true, "tosidebar": true, "console": false, "tostatus": false, "complete": "true", "targetType": "full", "statusVal": "", "statusType": "auto", "x": 520, "y": 760, "wires": [] }, { "id": "d3b1d8b5c05c2b22", "type": "debug", "z": "8914237e0d8e46c7", "g": "b07d4a44ddb86adf", "name": "debug 12", "active": false, "tosidebar": true, "console": false, "tostatus": false, "complete": "payload", "targetType": "msg", "statusVal": "", "statusType": "auto", "x": 840, "y": 840, "wires": [] }, { "id": "07bde3d3f85f2a40", "type": "inject", "z": "8914237e0d8e46c7", "g": "b07d4a44ddb86adf", "name": "", "props": [ { "p": "serverTimestamp", "v": "", "vt": "date" }, { "p": "sourceTimestamp", "v": "", "vt": "date" }, { "p": "payload" } ], "repeat": "", "crontab": "", "once": false, "onceDelay": 0.1, "topic": "", "payload": "1", "payloadType": "str", "x": 310, "y": 840, "wires": [ [ "20844959a8ea715c" ] ] }, { "id": "c369c3262a3b57fa", "type": "inject", "z": "8914237e0d8e46c7", "g": "b07d4a44ddb86adf", "name": "", "props": [ { "p": "serverTimestamp", "v": "", "vt": "date" }, { "p": "sourceTimestamp", "v": "", "vt": "date" }, { "p": "payload" } ], "repeat": "", "crontab": "", "once": false, "onceDelay": 0.1, "topic": "", "payload": "0", "payloadType": "str", "x": 310, "y": 880, "wires": [ [ "20844959a8ea715c" ] ] }, { "id": "7619f93616149cdc", "type": "function", "z": "8914237e0d8e46c7", "g": "11f19caa7b9760b7", "name": "gather & repeat", "func": "/**\n * This function node receives a monitored DataValue from OPC UA.\n * - msg.payload: Contains the value to be parsed into a float.\n * - msg.sourceTimestamp: Contains the value's timestamp in UTC ISO string format.\n *\n * It outputs messages to the first output with the following payload structure:\n * {\n * timestamp: new Date(msg.sourceTimestamp),\n * value: String(parseFloat(msg.payload))\n * }\n *\n * OPC UA only sends data when a value changes. To maintain a steady data stream, this node\n * repeats the last received value every second. The first repeated message is sent 1.5 seconds\n * after the initial message, with timestamps incrementing by the 1-second interval.\n *\n * Key constraints:\n * - Timestamps must always progress forward.\n * - Timing is based solely on OPC UA timestamps to avoid clock skew issues.\n * - Maintain only a single active timer to prevent duplicate messages.\n * - Timers are cleared properly upon node redeploy.\n */\n\nconst INTERVAL_MS = 1000; // 1-second interval for repeats\nconst INITIAL_DELAY_MS = 1500; // 1.5-second initial delay\n\n// Initialize context variables\nlet timer = context.get('timer');\nlet lastSentTimestamp = context.get('lastSentTimestamp') || 0;\nlet lastValue = context.get('lastValue');\nlet nextExpectedTimestamp = context.get('nextExpectedTimestamp') || 0;\n\n// Cleanup on node redeploy\nif (!context.get('initialized')) {\n node.on('close', function () {\n if (timer) {\n clearTimeout(timer);\n context.set('timer', null);\n }\n });\n context.set('initialized', true);\n}\n\nfunction sendAndSchedule(value, sourceTime) {\n // Send the message immediately\n node.send({\n payload: {\n timestamp: new Date(sourceTime),\n value: value\n }\n });\n\n // Update context\n context.set('lastSentTimestamp', sourceTime);\n context.set('lastValue', value);\n\n // Calculate the next expected timestamp\n const nextTs = sourceTime + INITIAL_DELAY_MS + INTERVAL_MS;\n context.set('nextExpectedTimestamp', nextTs);\n\n // Schedule the next repetition\n const delayUntilNextRepeat = INITIAL_DELAY_MS;\n timer = setTimeout(() => {\n sendRepeatedValue(value, nextTs, INTERVAL_MS);\n }, delayUntilNextRepeat);\n context.set('timer', timer);\n}\n\nfunction sendRepeatedValue(value, nextTs, interval) {\n // Send the repeated message\n node.send({\n payload: {\n timestamp: new Date(nextTs),\n value: value\n }\n });\n\n // Schedule the next repetition\n const nextNextTs = nextTs + interval;\n context.set('nextExpectedTimestamp', nextNextTs);\n\n timer = setTimeout(() => {\n sendRepeatedValue(value, nextNextTs, interval);\n }, interval);\n context.set('timer', timer);\n}\n\nif (msg.payload !== undefined && msg.sourceTimestamp) {\n // Clear any existing timer\n if (timer) {\n clearTimeout(timer);\n context.set('timer', null);\n }\n\n try {\n let sourceTime = new Date(msg.sourceTimestamp).getTime();\n const value = \"\" + parseInt(msg.payload);\n\n // Ensure timestamps always go forward\n if (lastSentTimestamp && sourceTime <= lastSentTimestamp) {\n sourceTime = lastSentTimestamp + 1; // Force it to be strictly greater\n }\n\n // Send immediately and schedule the first repeat\n sendAndSchedule(value, sourceTime);\n } catch (e) {\n node.error(\"Error processing message: \" + e.message, msg);\n }\n}", "outputs": 1, "timeout": 0, "noerr": 0, "initialize": "\n", "finalize": "", "libs": [], "x": 1400, "y": 840, "wires": [ [ "24ab918428d73e5f" ] ] }, { "id": "f1f77b6afa4257f0", "type": "inject", "z": "8914237e0d8e46c7", "g": "b07d4a44ddb86adf", "name": "Clear ", "props": [ { "p": "topic", "vt": "str" }, { "p": "payload" } ], "repeat": "", "crontab": "", "once": false, "onceDelay": 0.1, "topic": "[]", "payload": "[]", "payloadType": "str", "x": 690, "y": 740, "wires": [ [ "68e0c3653588a07d", "d3b1d8b5c05c2b22" ] ] }, { "id": "c9717ac11b53672e", "type": "function", "z": "8914237e0d8e46c7", "g": "62d148a7e9f5c520", "name": "enum to string", "func": "switch(msg.payload) {\n case 0:\n msg.payload = \"Off\";\n break;\n case 1:\n msg.payload = \"Standby\";\n break;\n case 2:\n msg.payload = \"Error\";\n break;\n case 3:\n msg.payload = \"Cleaning\";\n break;\n case 4:\n msg.payload = \"Serving Coffee\";\n break;\n case 5:\n msg.payload = \"Under Maintenance\";\n break;\n default:\n msg.payload = msg.payload;\n break;\n\n}\n// msg.payload = msg.payload;\nreturn msg;\n\n/*\n\nexport enum EnumCoffeeMachineModeEx {\n Off = 0,\n Standby = 1,\n Error = 2,\n Cleaning = 3,\n\n // not in spec !!!\n ServingCoffee = 4,\n\n UnderMaintenance = 5, // like filling coffee\n\n}\n*/", "outputs": 1, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 1360, "y": 740, "wires": [ [ "798535950531337a" ] ] }, { "id": "3a8c7eb7dc515e34", "type": "OpcUa-Client2-Monitor", "z": "8914237e0d8e46c7", "g": "62d148a7e9f5c520", "inputs": 0, "output": 2, "name": "CurrentState", "endpoint": "9593045b235c321c", "subscription": "s2", "startImmediately": true, "nodeId": "/di:DeviceSet/ns1:CoffeeMachineA.kitchen:Parameters.kitchen:CurrentState", "samplingInterval": "100", "discardOldest": true, "queueSize": 10, "x": 1150, "y": 740, "wires": [ [ "c9717ac11b53672e" ] ] }, { "id": "1c3d6230a57c852c", "type": "OpcUa-Client2-Call", "z": "8914237e0d8e46c7", "g": "a872632f0795dffe", "inputs": 1, "output": 4, "name": "Start", "endpoint": "9593045b235c321c", "action": "read", "methodId": "/di:DeviceSet/ns1:CoffeeMachineA.di:MethodSet.sterfive:Start", "objectId": "/di:DeviceSet/ns1:CoffeeMachineA", "x": 1450, "y": 420, "wires": [ [] ] }, { "id": "c172f527ba188866", "type": "inject", "z": "8914237e0d8e46c7", "g": "a872632f0795dffe", "name": "", "props": [], "repeat": "", "crontab": "", "once": false, "onceDelay": 0.1, "topic": "", "x": 1230, "y": 420, "wires": [ [ "1c3d6230a57c852c" ] ] }, { "id": "83de8924a1f6f2a5", "type": "OpcUa-Client2-Call", "z": "8914237e0d8e46c7", "g": "a872632f0795dffe", "inputs": 1, "output": 4, "name": "Stop", "endpoint": "9593045b235c321c", "action": "read", "methodId": "/di:DeviceSet/ns1:CoffeeMachineA.di:MethodSet.sterfive:Stop", "objectId": "/di:DeviceSet/ns1:CoffeeMachineA", "x": 1440, "y": 520, "wires": [ [] ] }, { "id": "807487cd9637d0e1", "type": "inject", "z": "8914237e0d8e46c7", "g": "a872632f0795dffe", "name": "", "props": [], "repeat": "", "crontab": "", "once": false, "onceDelay": 0.1, "topic": "", "x": 1230, "y": 520, "wires": [ [ "83de8924a1f6f2a5" ] ] }, { "id": "36b2628c69b87d42", "type": "OpcUa-Client2-Read", "z": "8914237e0d8e46c7", "g": "23e92d93ed23febc", "inputs": 1, "output": 4, "name": "", "endpoint": "9593045b235c321c", "attributeId": "Value", "nodeId": "", "outputType": "StatusCode", "x": 590, "y": 1080, "wires": [ [ "46d99d7986a03b57", "e975667424fe78b7" ] ] }, { "id": "46d99d7986a03b57", "type": "function", "z": "8914237e0d8e46c7", "g": "23e92d93ed23febc", "name": "Extract connection status", "func": "msg.payload = `${msg.payload} - ${msg.statusCode.toString()}`;\nreturn msg;", "outputs": 1, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 830, "y": 1080, "wires": [ [ "dcf7ab764b7d2921" ] ] }, { "id": "e975667424fe78b7", "type": "delay", "z": "8914237e0d8e46c7", "g": "23e92d93ed23febc", "name": "", "pauseType": "delay", "timeout": "1", "timeoutUnits": "seconds", "rate": "1", "nbRateUnits": "1", "rateUnits": "second", "randomFirst": "1", "randomLast": "5", "randomUnits": "seconds", "drop": false, "allowrate": false, "outputs": 1, "x": 480, "y": 980, "wires": [ [ "f79e321ce9b22631" ] ] }, { "id": "dc5e73eb1991eed3", "type": "inject", "z": "8914237e0d8e46c7", "g": "23e92d93ed23febc", "name": "", "props": [], "repeat": "", "crontab": "", "once": true, "onceDelay": "1", "topic": "", "x": 290, "y": 1100, "wires": [ [ "36b2628c69b87d42" ] ] }, { "id": "f79e321ce9b22631", "type": "function", "z": "8914237e0d8e46c7", "g": "23e92d93ed23febc", "name": "remove payload", "func": "msg.payload = null;\nreturn msg;", "outputs": 1, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 360, "y": 1060, "wires": [ [ "36b2628c69b87d42" ] ] }, { "id": "6c7658b34fce0ac3", "type": "comment", "z": "8914237e0d8e46c7", "g": "a872632f0795dffe", "name": "Buttons", "info": "", "x": 1190, "y": 160, "wires": [] }, { "id": "9326ae6929032e87", "type": "comment", "z": "8914237e0d8e46c7", "name": "Coffee Machine ( visit http://127.0.0.1:1880/dashboard/CoffeeMachineSynopsis )", "info": "# Coffee Machine *\n\nThis example demostrate how to \ncreate a Dashboard that interacts \nwith a OPCUA CoffeeMachine\n\n", "x": 480, "y": 40, "wires": [] }, { "id": "b577d7342c008353", "type": "OpcUa-Client2-Explore", "z": "8914237e0d8e46c7", "g": "a9f1eb481f74dde3", "inputs": 1, "output": 4, "name": "", "endpoint": "9593045b235c321c", "nodeId": "/di:DeviceSet/ns1:CoffeeMachineA.kitchen:Parameters", "outputType": "Value", "excludeEmpty": false, "followOrganizes": false, "x": 460, "y": 1260, "wires": [ [ "af5560e53daca7e6", "96aded34d2e12af9" ] ] }, { "id": "529de43bbcc8ddd4", "type": "inject", "z": "8914237e0d8e46c7", "g": "a9f1eb481f74dde3", "name": "", "props": [], "repeat": "10", "crontab": "", "once": false, "onceDelay": "2", "topic": "", "x": 300, "y": 1260, "wires": [ [ "b577d7342c008353" ] ] }, { "id": "73a7a87a437b298c", "type": "csv", "z": "8914237e0d8e46c7", "g": "a9f1eb481f74dde3", "name": "", "spec": "rfc", "sep": ";", "hdrin": "", "hdrout": "once", "multi": "one", "ret": "\\n", "temp": "", "skip": "0", "strings": true, "include_empty_strings": "", "include_null_values": "", "x": 990, "y": 1260, "wires": [ [ "239601af1e96d917", "ccd602507a4abc68" ] ] }, { "id": "239601af1e96d917", "type": "file", "z": "8914237e0d8e46c7", "g": "a9f1eb481f74dde3", "name": "Append to Output.csv", "filename": "/tmp/output.csv", "filenameType": "str", "appendNewline": false, "createDir": false, "overwriteFile": "false", "encoding": "none", "x": 1200, "y": 1260, "wires": [ [] ] }, { "id": "ccd602507a4abc68", "type": "debug", "z": "8914237e0d8e46c7", "g": "a9f1eb481f74dde3", "name": "debug 13", "active": false, "tosidebar": true, "console": false, "tostatus": false, "complete": "payload", "targetType": "msg", "statusVal": "", "statusType": "auto", "x": 1160, "y": 1300, "wires": [] }, { "id": "d21be93e03b60165", "type": "OpcUa-Client2-Explore", "z": "8914237e0d8e46c7", "g": "230bb82688833807", "inputs": 1, "output": 4, "name": "", "endpoint": "9593045b235c321c", "nodeId": "/di:DeviceSet/ns1:CoffeeMachineA.kitchen:Parameters", "outputType": "NodeId", "excludeEmpty": false, "followOrganizes": false, "x": 440, "y": 1540, "wires": [ [ "50c33de026727ab2" ] ] }, { "id": "1832b372c56dc715", "type": "inject", "z": "8914237e0d8e46c7", "g": "230bb82688833807", "name": "", "props": [], "repeat": "", "crontab": "", "once": false, "onceDelay": "2", "topic": "", "x": 290, "y": 1540, "wires": [ [ "d21be93e03b60165" ] ] }, { "id": "5f6eeb0fc5228a66", "type": "OpcUa-Client2-Read", "z": "8914237e0d8e46c7", "g": "230bb82688833807", "inputs": 1, "output": 4, "name": "", "endpoint": "9593045b235c321c", "attributeId": "Value", "nodeId": "", "outputType": "Value", "x": 870, "y": 1540, "wires": [ [ "fe845d4c6aa473d0", "829ce0410d7c3b05", "5e50f3a2da48a03b" ] ] }, { "id": "5e50f3a2da48a03b", "type": "debug", "z": "8914237e0d8e46c7", "g": "230bb82688833807", "name": "debug 15", "active": false, "tosidebar": true, "console": false, "tostatus": false, "complete": "true", "targetType": "full", "statusVal": "", "statusType": "auto", "x": 1260, "y": 1460, "wires": [] }, { "id": "fe845d4c6aa473d0", "type": "delay", "z": "8914237e0d8e46c7", "g": "230bb82688833807", "name": "", "pauseType": "delay", "timeout": "2", "timeoutUnits": "seconds", "rate": "1", "nbRateUnits": "1", "rateUnits": "second", "randomFirst": "1", "randomLast": "5", "randomUnits": "seconds", "drop": false, "allowrate": false, "outputs": 1, "x": 840, "y": 1480, "wires": [ [ "99b27a72cfb58370" ] ] }, { "id": "50c33de026727ab2", "type": "function", "z": "8914237e0d8e46c7", "g": "230bb82688833807", "name": "memorize payload", "func": "flow.set(\"p\", msg.payload)\nreturn msg;", "outputs": 1, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 630, "y": 1540, "wires": [ [ "5f6eeb0fc5228a66" ] ] }, { "id": "99b27a72cfb58370", "type": "function", "z": "8914237e0d8e46c7", "g": "230bb82688833807", "name": "function 3", "func": "msg.payload = flow.get(\"p\");\nreturn msg;", "outputs": 1, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 955, "y": 1480, "wires": [ [ "5f6eeb0fc5228a66" ] ], "l": false }, { "id": "829ce0410d7c3b05", "type": "function", "z": "8914237e0d8e46c7", "g": "230bb82688833807", "name": "clean up", "func": "msg.timestamp = new Date(); // new Date(msg.sourceTimestamp);\nmsg.payload.TotalMix = msg.payload.ServedCoffeeCount;\nmsg.payload.timestamp = msg.timestamp;\nmsg.payload.d= msg.timestamp.toISOString();\nmsg.payload.values = [ Math.random()*1000 , Math.random()*1000];\nmsg.payload.titles = [ \"A\", \"B\"]\nreturn msg;", "outputs": 1, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 1100, "y": 1540, "wires": [ [ "5e50f3a2da48a03b", "1bea16ead1291025" ] ] }, { "id": "4cecc03d1a64c091", "type": "OpcUa-Client2-Browse", "z": "8914237e0d8e46c7", "g": "ec1ab7ca9e060d7f", "inputs": 1, "output": 4, "name": "", "endpoint": "9593045b235c321c", "nodeId": "/di:DeviceSet/ns1:CoffeeMachineA.sterfive:Recipes", "referenceTypeId": "HasChild", "includeSubtypes": false, "browseDirection": "Forward", "nodeClassMask": "Object", "resultMask": "ReferenceType | BrowseName | TypeDefinition | DisplayName | NodeClass | IsForward", "x": 1320, "y": 980, "wires": [ [ "57622c7f24dacef7" ] ] }, { "id": "f6b1e7b0df0fa288", "type": "debug", "z": "8914237e0d8e46c7", "g": "ec1ab7ca9e060d7f", "name": "debug 16", "active": true, "tosidebar": true, "console": false, "tostatus": false, "complete": "true", "targetType": "full", "statusVal": "", "statusType": "auto", "x": 1640, "y": 1040, "wires": [] }, { "id": "0c098347de28a15d", "type": "inject", "z": "8914237e0d8e46c7", "g": "ec1ab7ca9e060d7f", "name": "", "props": [], "repeat": "", "crontab": "", "once": true, "onceDelay": "1", "topic": "", "x": 1150, "y": 980, "wires": [ [ "4cecc03d1a64c091" ] ] }, { "id": "57622c7f24dacef7", "type": "function", "z": "8914237e0d8e46c7", "g": "ec1ab7ca9e060d7f", "name": "extract recipe name", "func": "msg.payload = msg.payload.map(ref => ref.displayName );\nreturn msg;", "outputs": 1, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 1510, "y": 980, "wires": [ [ "e3c1066f3260a1cb" ] ] }, { "id": "e3c1066f3260a1cb", "type": "function", "z": "8914237e0d8e46c7", "g": "ec1ab7ca9e060d7f", "name": "prepare combo box options", "func": "msg.ui_update = {\n options: msg.payload\n};\nmsg.payload = msg.payload[0].value;\nreturn msg;", "outputs": 1, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 1240, "y": 1060, "wires": [ [ "f6b1e7b0df0fa288", "237e980896f1a684" ] ] }, { "id": "0597a0ffcdab77cd", "type": "function", "z": "8914237e0d8e46c7", "g": "ec1ab7ca9e060d7f", "name": "memorize selectedComboValue", "func": "// This function is triggered whenever the combo box value changes.\n// It stores the selected value in the flow context.\nflow.set(\"selectedComboValue\", msg.payload);\nreturn null; // No output required from this node.\n\nreturn msg;", "outputs": 1, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 1710