@jsgorana/node-red-opcua 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,63 @@
1
+ <script type="text/javascript">
2
+ RED.nodes.registerType("opcua-write", {
3
+ category: "OPC-UA",
4
+ color: "#4a90d9",
5
+ defaults: {
6
+ name: { value: "" },
7
+ endpoint: { value: "", type: "opcua-endpoint", required: true },
8
+ nodeId: { value: "" },
9
+ dataType: { value: "Double" }
10
+ },
11
+ inputs: 1,
12
+ outputs: 1,
13
+ icon: "font-awesome/fa-upload",
14
+ label: function () {
15
+ return this.name || (this.nodeId ? "write " + this.nodeId : "opcua-write");
16
+ }
17
+ });
18
+ </script>
19
+
20
+ <script type="text/html" data-template-name="opcua-write">
21
+ <div class="form-row">
22
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
23
+ <input type="text" id="node-input-name" placeholder="Name">
24
+ </div>
25
+ <div class="form-row">
26
+ <label for="node-input-endpoint"><i class="fa fa-server"></i> Endpoint</label>
27
+ <input type="text" id="node-input-endpoint">
28
+ </div>
29
+ <div class="form-row">
30
+ <label for="node-input-nodeId"><i class="fa fa-hashtag"></i> Node ID</label>
31
+ <input type="text" id="node-input-nodeId" placeholder="ns=1;s=Temperature">
32
+ </div>
33
+ <div class="form-row">
34
+ <label for="node-input-dataType"><i class="fa fa-cube"></i> Data Type</label>
35
+ <select id="node-input-dataType">
36
+ <option value="Boolean">Boolean</option>
37
+ <option value="SByte">SByte</option>
38
+ <option value="Byte">Byte</option>
39
+ <option value="Int16">Int16</option>
40
+ <option value="UInt16">UInt16</option>
41
+ <option value="Int32">Int32</option>
42
+ <option value="UInt32">UInt32</option>
43
+ <option value="Float">Float</option>
44
+ <option value="Double">Double</option>
45
+ <option value="String">String</option>
46
+ <option value="DateTime">DateTime</option>
47
+ </select>
48
+ </div>
49
+ </script>
50
+
51
+ <script type="text/html" data-help-name="opcua-write">
52
+ <p>Writes <code>msg.payload</code> to the Value attribute of an OPC-UA node.</p>
53
+ <h3>Inputs</h3>
54
+ <dl class="message-properties">
55
+ <dt>payload</dt><dd>The value to write.</dd>
56
+ <dt class="optional">nodeId <span class="property-type">string</span></dt><dd>Overrides the configured Node ID.</dd>
57
+ <dt class="optional">dataType <span class="property-type">string</span></dt><dd>Overrides the configured data type.</dd>
58
+ </dl>
59
+ <h3>Outputs</h3>
60
+ <dl class="message-properties">
61
+ <dt>statusCode <span class="property-type">string</span></dt><dd>OPC-UA write status code.</dd>
62
+ </dl>
63
+ </script>
@@ -0,0 +1,110 @@
1
+ "use strict";
2
+
3
+ const { AttributeIds, DataType, Variant, StatusCodes } = require("node-opcua");
4
+
5
+ const STATUS_MAP = {
6
+ connecting: { fill: "yellow", shape: "ring", text: "connecting" },
7
+ connected: { fill: "green", shape: "dot", text: "connected" },
8
+ reconnecting: { fill: "yellow", shape: "ring", text: "reconnecting" },
9
+ error: { fill: "red", shape: "ring", text: "error" },
10
+ closed: { fill: "grey", shape: "ring", text: "closed" }
11
+ };
12
+
13
+ function variantFor(dataTypeName, value) {
14
+ const dataType = DataType[dataTypeName];
15
+ if (dataType === undefined) {
16
+ throw new Error("Unknown dataType: " + dataTypeName);
17
+ }
18
+ return new Variant({ dataType: dataType, value: value });
19
+ }
20
+
21
+ function resolveWrites(node, msg) {
22
+ if (Array.isArray(msg.payload)) {
23
+ return msg.payload.map((item, index) => {
24
+ if (item && typeof item === "object" && !Array.isArray(item)) {
25
+ return {
26
+ nodeId: item.nodeId || item.topic,
27
+ value: Object.prototype.hasOwnProperty.call(item, "value") ? item.value : item.payload,
28
+ dataType: item.dataType || msg.dataType || node.dataType
29
+ };
30
+ }
31
+ return {
32
+ nodeId: Array.isArray(msg.nodeIds) ? msg.nodeIds[index] : undefined,
33
+ value: item,
34
+ dataType: Array.isArray(msg.dataTypes) ? msg.dataTypes[index] : (msg.dataType || node.dataType)
35
+ };
36
+ });
37
+ }
38
+ return [{
39
+ nodeId: msg.nodeId || node.nodeId,
40
+ value: msg.payload,
41
+ dataType: msg.dataType || node.dataType
42
+ }];
43
+ }
44
+
45
+ function toWriteValue(write) {
46
+ if (!write.nodeId) throw new Error("No nodeId provided");
47
+ return {
48
+ nodeId: write.nodeId,
49
+ attributeId: AttributeIds.Value,
50
+ value: {
51
+ value: variantFor(write.dataType, write.value)
52
+ }
53
+ };
54
+ }
55
+
56
+ module.exports = function (RED) {
57
+ function OpcuaWriteNode(config) {
58
+ RED.nodes.createNode(this, config);
59
+ const node = this;
60
+ node.endpoint = RED.nodes.getNode(config.endpoint);
61
+ node.nodeId = config.nodeId;
62
+ node.dataType = config.dataType || "Double";
63
+
64
+ if (!node.endpoint) {
65
+ node.status({ fill: "red", shape: "ring", text: "no endpoint" });
66
+ return;
67
+ }
68
+ const manager = node.endpoint.register();
69
+ const unsubscribeState = manager.onState((state) => node.status(STATUS_MAP[state] || {}));
70
+
71
+ node.on("input", async function (msg, send, done) {
72
+ try {
73
+ const writes = resolveWrites(node, msg);
74
+ if (writes.length === 0) throw new Error("No writes provided");
75
+ const writeValues = writes.map(toWriteValue);
76
+
77
+ const statusCodes = await manager.runWithTimeout(async () => {
78
+ const session = await manager.getSession();
79
+ return session.write(writeValues.length === 1 ? writeValues[0] : writeValues);
80
+ }, "write");
81
+
82
+ if (writeValues.length === 1) {
83
+ msg.statusCode = statusCodes.toString();
84
+ msg.nodeId = writes[0].nodeId;
85
+ if (statusCodes !== StatusCodes.Good) {
86
+ node.status({ fill: "yellow", shape: "ring", text: statusCodes.name });
87
+ }
88
+ } else {
89
+ msg.statusCodes = statusCodes.map((s) => s.toString());
90
+ msg.nodeIds = writes.map((w) => w.nodeId);
91
+ if (statusCodes.some((s) => s !== StatusCodes.Good)) {
92
+ node.status({ fill: "yellow", shape: "ring", text: "partial write" });
93
+ }
94
+ }
95
+ send(msg);
96
+ done();
97
+ } catch (err) {
98
+ node.status({ fill: "red", shape: "ring", text: "write error" });
99
+ done(err);
100
+ }
101
+ });
102
+
103
+ node.on("close", async function (done) {
104
+ unsubscribeState();
105
+ await node.endpoint.deregister();
106
+ done();
107
+ });
108
+ }
109
+ RED.nodes.registerType("opcua-write", OpcuaWriteNode);
110
+ };
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "@jsgorana/node-red-opcua",
3
+ "version": "0.1.0",
4
+ "description": "Lean, modern OPC-UA client and server nodes for Node-RED 5.0 — read, write, browse, subscribe, and expose Node-RED data as an OPC UA server.",
5
+ "keywords": [
6
+ "node-red",
7
+ "opcua",
8
+ "opc-ua",
9
+ "iiot",
10
+ "industrial",
11
+ "automation",
12
+ "scada"
13
+ ],
14
+ "license": "MIT",
15
+ "author": "jsgorana <jsgorana@gmail.com>",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/jsgorana/node-red-opcua.git"
19
+ },
20
+ "bugs": {
21
+ "url": "https://github.com/jsgorana/node-red-opcua/issues"
22
+ },
23
+ "homepage": "https://github.com/jsgorana/node-red-opcua#readme",
24
+ "engines": {
25
+ "node": ">=22.9.0"
26
+ },
27
+ "node-red": {
28
+ "version": ">=4.0.0",
29
+ "nodes": {
30
+ "opcua-endpoint": "nodes/opcua-endpoint.js",
31
+ "opcua-read": "nodes/opcua-read.js",
32
+ "opcua-write": "nodes/opcua-write.js",
33
+ "opcua-browse": "nodes/opcua-browse.js",
34
+ "opcua-subscribe": "nodes/opcua-subscribe.js",
35
+ "opcua-server": "nodes/opcua-server.js"
36
+ }
37
+ },
38
+ "scripts": {
39
+ "test": "mocha \"test/**/*_spec.js\" --timeout 20000",
40
+ "lint": "eslint nodes test"
41
+ },
42
+ "dependencies": {
43
+ "node-opcua": "^2.173.1"
44
+ },
45
+ "devDependencies": {
46
+ "@eslint/js": "^10.0.1",
47
+ "eslint": "^10.6.0",
48
+ "globals": "^17.7.0",
49
+ "mocha": "^10.7.0",
50
+ "node-red": "^5.0.0",
51
+ "node-red-node-test-helper": "^0.3.4",
52
+ "should": "^13.2.3"
53
+ },
54
+ "files": [
55
+ "nodes",
56
+ "examples",
57
+ "docs/assets",
58
+ "README.md",
59
+ "CHANGELOG.md",
60
+ "CONTRIBUTING.md",
61
+ "LICENSE"
62
+ ]
63
+ }