UNPKG

@fiqch/n8n-nodes-fiq-opcua

Version:

n8n custom node for reading and writing OPC UA data (FactoryIQ)

617 lines 31.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.FactoryiqOpcUa = void 0; const n8n_workflow_1 = require("n8n-workflow"); const vendor_1 = require("../../../vendor"); const ConnectionPool_1 = require("./ConnectionPool"); class FactoryiqOpcUa { constructor() { this.description = { displayName: 'FactoryIQ OpcUA', name: 'factoryiqOpcUa', group: ['input', 'output'], version: 1, description: 'Read and write data to OPC UA servers', icon: 'file:FactoryIQ.svg', defaults: { name: 'OPC UA', }, inputs: ["main"], outputs: ["main"], usableAsTool: true, credentials: [ { name: 'opcUaCredentialsApi', required: true, testedBy: 'opcuaConnectionTest', }, ], properties: [ { displayName: 'Action', name: 'operation', type: 'options', options: [ { name: 'Read', value: 'read' }, { name: 'Write', value: 'write' }, ], default: 'read', description: 'Choose whether to read or write OPC UA data', noDataExpression: true, }, { displayName: 'Node IDs', name: 'nodeIds', type: 'string', typeOptions: { multipleValues: true, multipleValueButtonText: 'Add NodeId', }, default: [], placeholder: 'e.g. ns=1;s=Temperature', description: 'Add one or more OPC UA nodeIds to read. Each nodeId should be entered as a separate entry (e.g., ns=1;s=Temperature).', displayOptions: { show: { operation: ['read'], }, }, }, { displayName: 'Write Operation', name: 'writeOperation', type: 'options', options: [ { name: 'Write Variable', value: 'writeVariable' }, { name: 'Call Method', value: 'callMethod' }, ], default: 'writeVariable', description: 'Choose whether to write to a variable or call a method', displayOptions: { show: { operation: ['write'], }, }, noDataExpression: true, }, { displayName: 'Node ID', name: 'nodeId', type: 'string', displayOptions: { show: { operation: ['write'], writeOperation: ['writeVariable'] } }, required: true, description: 'The OPC UA nodeId of the variable to write to', default: '', }, { displayName: 'Value', name: 'value', type: 'string', displayOptions: { show: { operation: ['write'], writeOperation: ['writeVariable'] } }, required: true, description: 'The value to write', default: '', }, { displayName: 'Data Type', name: 'dataType', type: 'options', displayOptions: { show: { operation: ['write'], writeOperation: ['writeVariable'] } }, options: [ { name: 'Boolean', value: 'Boolean' }, { name: 'Byte', value: 'Byte' }, { name: 'ByteString', value: 'ByteString' }, { name: 'DateTime', value: 'DateTime' }, { name: 'Double', value: 'Double' }, { name: 'Float', value: 'Float' }, { name: 'Guid', value: 'Guid' }, { name: 'Int16', value: 'Int16' }, { name: 'Int32', value: 'Int32' }, { name: 'Int64', value: 'Int64' }, { name: 'SByte', value: 'SByte' }, { name: 'String', value: 'String' }, { name: 'UInt16', value: 'UInt16' }, { name: 'UInt32', value: 'UInt32' }, { name: 'UInt64', value: 'UInt64' }, ], required: true, description: 'The OPC UA data type of the value', default: 'Boolean', }, { displayName: 'Object Node ID', name: 'objectNodeId', type: 'string', displayOptions: { show: { operation: ['write'], writeOperation: ['callMethod'] } }, required: true, description: 'The nodeId of the object containing the method', default: '', }, { displayName: 'Method Node ID', name: 'methodNodeId', type: 'string', displayOptions: { show: { operation: ['write'], writeOperation: ['callMethod'] } }, required: true, description: 'The nodeId of the method to call', default: '', }, { displayName: 'Parameters', name: 'parameters', type: 'fixedCollection', displayOptions: { show: { operation: ['write'], writeOperation: ['callMethod'] } }, description: 'Optional. Add input arguments for the method call. Leave empty for methods with no input arguments.', default: {}, options: [ { displayName: 'Argument', name: 'argument', type: 'collection', placeholder: 'Add Argument', default: {}, options: [ { displayName: 'Data Type', name: 'dataType', type: 'options', options: [ { name: 'Boolean', value: 'Boolean' }, { name: 'Byte', value: 'Byte' }, { name: 'ByteString', value: 'ByteString' }, { name: 'DateTime', value: 'DateTime' }, { name: 'Double', value: 'Double' }, { name: 'Float', value: 'Float' }, { name: 'Guid', value: 'Guid' }, { name: 'Int16', value: 'Int16' }, { name: 'Int32', value: 'Int32' }, { name: 'Int64', value: 'Int64' }, { name: 'SByte', value: 'SByte' }, { name: 'String', value: 'String' }, { name: 'UInt16', value: 'UInt16' }, { name: 'UInt32', value: 'UInt32' }, { name: 'UInt64', value: 'UInt64' }, ], default: 'String', description: 'The OPC UA data type of the argument', }, { displayName: 'Value', name: 'value', type: 'string', default: '', description: 'The value for this argument', }, ], }, ], }, ], }; this.methods = { credentialTest: { async opcuaConnectionTest(credential) { const credentials = credential.data; if (!credentials) { return { status: 'Error', message: 'No credentials provided.', }; } let client; try { const { OPCUAClient, SecurityPolicy, MessageSecurityMode, UserTokenType } = require('../../../vendor'); const endpointUrl = credentials.endpointUrl; if (!endpointUrl || typeof endpointUrl !== 'string' || endpointUrl.trim() === '') { return { status: 'Error', message: 'Endpoint URL is required and cannot be empty.', }; } if (!endpointUrl.startsWith('opc.tcp://') && !endpointUrl.startsWith('opc.https://')) { return { status: 'Error', message: `Endpoint URL must start with opc.tcp:// or opc.https://. Received: "${endpointUrl}"`, }; } const securityPolicy = credentials.securityPolicy || 'None'; const securityMode = credentials.securityMode || 'None'; const authenticationType = credentials.authenticationType || 'anonymous'; let securityPolicyEnum = SecurityPolicy.None; let securityModeEnum = MessageSecurityMode.None; switch (securityPolicy) { case 'Basic256Sha256': securityPolicyEnum = SecurityPolicy.Basic256Sha256; break; case 'Basic256': securityPolicyEnum = SecurityPolicy.Basic256; break; case 'Basic128Rsa15': securityPolicyEnum = SecurityPolicy.Basic128Rsa15; break; case 'Aes128_Sha256_RsaOaep': securityPolicyEnum = SecurityPolicy.Aes128_Sha256_RsaOaep; break; case 'Aes256_Sha256_RsaPss': securityPolicyEnum = SecurityPolicy.Aes256_Sha256_RsaPss; break; default: securityPolicyEnum = SecurityPolicy.None; } switch (securityMode) { case 'Sign': securityModeEnum = MessageSecurityMode.Sign; break; case 'SignAndEncrypt': case 'Sign & Encrypt': securityModeEnum = MessageSecurityMode.SignAndEncrypt; break; default: securityModeEnum = MessageSecurityMode.None; } const clientOptions = { securityPolicy: securityPolicyEnum, securityMode: securityModeEnum, connectionStrategy: { initialDelay: 1000, maxRetry: 1, maxDelay: 2000 }, clientName: 'n8n-opcua-credential-test', requestedSessionTimeout: 10000, endpointMustExist: false, securityOptions: { rejectUnauthorized: false }, }; if (authenticationType === 'x509') { if (credentials.certificate && credentials.privateKey) { clientOptions.certificateData = Buffer.from(String(credentials.certificate)); clientOptions.privateKeyData = Buffer.from(String(credentials.privateKey)); } else { throw new n8n_workflow_1.ApplicationError('X509 authentication requires both certificate and private key.'); } } client = OPCUAClient.create(clientOptions); await client.connect(endpointUrl); let userIdentity; if (authenticationType === 'usernamePassword') { userIdentity = { type: UserTokenType.UserName, userName: credentials.username, password: credentials.password, }; } else if (authenticationType === 'x509') { userIdentity = { type: UserTokenType.Certificate, certificateData: String(credentials.certificate), privateKey: String(credentials.privateKey), }; } else { userIdentity = undefined; } const session = await client.createSession(userIdentity); await session.close(); await client.disconnect(); } catch (e) { if (client) { try { await client.disconnect(); } catch { } } return { status: 'Error', message: e.message, }; } return { status: 'OK', message: 'Connection successful!', }; }, }, }; } async execute() { var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o; const items = this.getInputData(); const results = []; const operation = this.getNodeParameter('operation', 0); if (operation === 'read') { const nodeIdsRaw = this.getNodeParameter('nodeIds', 0, []); const nodeIds = Array.isArray(nodeIdsRaw) ? nodeIdsRaw : [nodeIdsRaw]; if (!Array.isArray(nodeIds) || nodeIds.length === 0 || !nodeIds[0]) { throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'At least one Node ID must be provided for reading.'); } const credentials = await this.getCredentials('opcUaCredentialsApi'); if (!credentials) { throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'No OPC UA credentials provided.'); } const authenticationType = credentials.authenticationType || 'anonymous'; if (authenticationType === 'x509' && (!credentials.certificate || !credentials.privateKey)) { throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'X509 authentication requires both certificate and private key.'); } let client, session, pooledConnection; try { const pool = ConnectionPool_1.OpcUaConnectionPool.getInstance(); pooledConnection = await pool.getConnection(credentials); client = pooledConnection.client; session = pooledConnection.session; } catch (error) { throw new n8n_workflow_1.NodeOperationError(this.getNode(), error, { message: 'Failed to connect or authenticate to OPC UA server.' }); } const clientName = pooledConnection ? `n8n-opcua-pool` : `n8n-opcua-${Math.random().toString(36).substring(2, 10)}`; try { const nodesToRead = nodeIds.map(nodeId => ({ nodeId, attributeId: vendor_1.AttributeIds.Value, })); const readResults = await session.read(nodesToRead); const metrics = {}; const metaDataTypes = {}; const errors = {}; let overallStatus = 'Good'; const timestamp = Date.now(); for (let i = 0; i < nodeIds.length; i++) { const nodeId = nodeIds[i]; const result = readResults[i]; if (result.statusCode && result.statusCode.name !== 'Good') { metrics[nodeId] = null; errors[nodeId] = result.statusCode.name; overallStatus = nodeIds.length === 1 ? result.statusCode.name : 'Partial'; } else { metrics[nodeId] = result.value ? result.value.value : null; if (result.value && typeof result.value.dataType === 'number' && FactoryiqOpcUa.opcuaDataTypeMap[result.value.dataType] !== undefined) { metaDataTypes[nodeId] = FactoryiqOpcUa.opcuaDataTypeMap[result.value.dataType]; } } } const output = { timestamp, source: clientName, protocol: 'opcua', address: nodeIds.length === 1 ? nodeIds[0] : `${nodeIds.length} nodes`, metrics, status: overallStatus, meta: nodeIds.length === 1 ? { dataType: metaDataTypes[nodeIds[0]] || null, ...(Object.keys(errors).length > 0 && { error: errors[nodeIds[0]] }) } : { dataTypes: metaDataTypes, ...(Object.keys(errors).length > 0 && { errors }), nodeCount: nodeIds.length, nodeIds: nodeIds }, }; results.push({ json: output }); } catch (error) { throw new n8n_workflow_1.NodeOperationError(this.getNode(), error, { message: 'Failed to read node values.' }); } finally { if (pooledConnection) { const pool = ConnectionPool_1.OpcUaConnectionPool.getInstance(); pool.releaseConnection(pooledConnection); } else { try { if (session) { await session.close(); } await client.disconnect(); } catch (error) { } } } return [results]; } else if (operation === 'write') { for (let itemIndex = 0; itemIndex < items.length; itemIndex++) { const writeOperation = this.getNodeParameter('writeOperation', itemIndex); if (!writeOperation) { throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Write operation must be specified.'); } if (writeOperation === 'writeVariable') { const nodeId = this.getNodeParameter('nodeId', itemIndex); const dataType = this.getNodeParameter('dataType', itemIndex); if (!nodeId || !dataType) { throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Node ID and Data Type are required for variable write.'); } } else if (writeOperation === 'callMethod') { } else { throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Unsupported operation type.'); } const credentials = await this.getCredentials('opcUaCredentialsApi'); if (!credentials) { throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'No OPC UA credentials provided.'); } const authenticationType = credentials.authenticationType || 'anonymous'; if (authenticationType === 'x509' && (!credentials.certificate || !credentials.privateKey)) { throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'X509 authentication requires both certificate and private key.'); } let client, session, pooledConnection; try { const pool = ConnectionPool_1.OpcUaConnectionPool.getInstance(); pooledConnection = await pool.getConnection(credentials); client = pooledConnection.client; session = pooledConnection.session; } catch (error) { throw new n8n_workflow_1.NodeOperationError(this.getNode(), error, { message: 'Failed to connect or authenticate to OPC UA server.' }); } const clientName = pooledConnection ? `n8n-opcua-pool` : `n8n-opcua-${Math.random().toString(36).substring(2, 10)}`; try { if (writeOperation === 'writeVariable') { const nodeId = this.getNodeParameter('nodeId', itemIndex); const value = this.getNodeParameter('value', itemIndex); const dataType = this.getNodeParameter('dataType', itemIndex); const dataTypeEnum = FactoryiqOpcUa.getDataTypeEnum(dataType); const writeValue = FactoryiqOpcUa.convertValueToDataType(value, dataType); const nodesToWrite = [{ nodeId, attributeId: vendor_1.AttributeIds.Value, value: { value: { dataType: dataTypeEnum, value: writeValue }, }, }]; const operationResult = await session.write(nodesToWrite); const output = { timestamp: Date.now(), source: clientName, protocol: 'opcua', address: nodeId, metrics: { [nodeId]: writeValue }, status: (((_a = operationResult[0]) === null || _a === void 0 ? void 0 : _a.name) || ((_b = operationResult[0]) === null || _b === void 0 ? void 0 : _b.toString())) === 'Good' ? 'ok' : 'error', meta: { dataType: (typeof dataTypeEnum === 'number' && FactoryiqOpcUa.opcuaDataTypeMap[dataTypeEnum] !== undefined) ? FactoryiqOpcUa.opcuaDataTypeMap[dataTypeEnum] : null, operationType: 'variable_write', statusCode: ((_c = operationResult[0]) === null || _c === void 0 ? void 0 : _c.name) || ((_d = operationResult[0]) === null || _d === void 0 ? void 0 : _d.toString()) }, }; results.push({ json: output }); } else if (writeOperation === 'callMethod') { const objectNodeId = this.getNodeParameter('objectNodeId', itemIndex); const methodNodeId = this.getNodeParameter('methodNodeId', itemIndex); const parametersRaw = this.getNodeParameter('parameters', itemIndex, {}); let inputArguments = []; if (parametersRaw && typeof parametersRaw === 'object' && 'arguments' in parametersRaw && Array.isArray(parametersRaw.arguments)) { inputArguments = parametersRaw.arguments.map((arg) => ({ dataType: arg.dataType, value: arg.value, })); } const methodCall = [{ objectId: objectNodeId, methodId: methodNodeId, inputArguments, }]; const operationResult = await session.call(methodCall); const output = { timestamp: Date.now(), source: clientName, protocol: 'opcua', address: methodNodeId, metrics: { outputArguments: ((_e = operationResult[0]) === null || _e === void 0 ? void 0 : _e.outputArguments) || [] }, status: (((_g = (_f = operationResult[0]) === null || _f === void 0 ? void 0 : _f.statusCode) === null || _g === void 0 ? void 0 : _g.name) || ((_j = (_h = operationResult[0]) === null || _h === void 0 ? void 0 : _h.statusCode) === null || _j === void 0 ? void 0 : _j.toString())) === 'Good' ? 'ok' : 'error', meta: { inputArguments, operationType: 'method_call', statusCode: ((_l = (_k = operationResult[0]) === null || _k === void 0 ? void 0 : _k.statusCode) === null || _l === void 0 ? void 0 : _l.name) || ((_o = (_m = operationResult[0]) === null || _m === void 0 ? void 0 : _m.statusCode) === null || _o === void 0 ? void 0 : _o.toString()) }, }; results.push({ json: output }); } else { throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Unsupported operation type.'); } } catch (error) { throw new n8n_workflow_1.NodeOperationError(this.getNode(), error, { message: 'Failed to execute operation on OPC UA node.' }); } finally { if (pooledConnection) { const pool = ConnectionPool_1.OpcUaConnectionPool.getInstance(); pool.releaseConnection(pooledConnection); } else { try { if (session) { await session.close(); } await client.disconnect(); } catch (error) { } } } } return [results]; } else { throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Invalid operation selected.'); } } static getDataTypeEnum(dataType) { switch (dataType) { case 'Boolean': return vendor_1.DataType.Boolean; case 'SByte': return vendor_1.DataType.SByte; case 'Byte': return vendor_1.DataType.Byte; case 'Int16': return vendor_1.DataType.Int16; case 'UInt16': return vendor_1.DataType.UInt16; case 'Int32': return vendor_1.DataType.Int32; case 'UInt32': return vendor_1.DataType.UInt32; case 'Int64': return vendor_1.DataType.Int64; case 'UInt64': return vendor_1.DataType.UInt64; case 'Float': return vendor_1.DataType.Float; case 'Double': return vendor_1.DataType.Double; case 'String': return vendor_1.DataType.String; case 'DateTime': return vendor_1.DataType.DateTime; case 'Guid': return vendor_1.DataType.Guid; case 'ByteString': return vendor_1.DataType.ByteString; default: return vendor_1.DataType.String; } } static convertValueToDataType(value, dataType) { switch (dataType) { case 'Boolean': return value.toString().toLowerCase() === 'true' || value === '1' || value.toString().toLowerCase() === 'on' || value.toString().toLowerCase() === 'yes'; case 'SByte': return Math.max(-128, Math.min(127, parseInt(value) || 0)); case 'Byte': return Math.max(0, Math.min(255, parseInt(value) || 0)); case 'Int16': return Math.max(-32768, Math.min(32767, parseInt(value) || 0)); case 'UInt16': return Math.max(0, Math.min(65535, parseInt(value) || 0)); case 'Int32': return Math.max(-2147483648, Math.min(2147483647, parseInt(value) || 0)); case 'UInt32': return Math.max(0, Math.min(4294967295, parseInt(value) || 0)); case 'Int64': return parseInt(value) || 0; case 'UInt64': return Math.max(0, parseInt(value) || 0); case 'Float': return parseFloat(value) || 0.0; case 'Double': return parseFloat(value) || 0.0; case 'String': return value.toString(); case 'DateTime': return new Date(value); case 'Guid': return value.toString(); case 'ByteString': return Buffer.from(value); default: return value.toString(); } } } exports.FactoryiqOpcUa = FactoryiqOpcUa; FactoryiqOpcUa.opcuaDataTypeMap = { 0: 'Null', 1: 'Boolean', 2: 'SByte', 3: 'Byte', 4: 'Int16', 5: 'UInt16', 6: 'Int32', 7: 'UInt32', 8: 'Int64', 9: 'UInt64', 10: 'Float', 11: 'Double', 12: 'String', 13: 'DateTime', 14: 'Guid', 15: 'ByteString', 16: 'XmlElement', 17: 'NodeId', 18: 'ExpandedNodeId', 19: 'StatusCode', 20: 'QualifiedName', 21: 'LocalizedText', 22: 'ExtensionObject', 23: 'DataValue', 24: 'Variant', 25: 'DiagnosticInfo', }; //# sourceMappingURL=FactoryiqOpcUa.node.js.map