├── nifi-opcua-service ├── src │ ├── test │ │ ├── resources │ │ │ ├── client.jks │ │ │ ├── trust.jks │ │ │ └── trust-wrong.jks │ │ └── java │ │ │ └── de │ │ │ └── fraunhofer │ │ │ └── fit │ │ │ └── opcua │ │ │ ├── TestProcessor.java │ │ │ └── TestStandardOPCUAService.java │ └── main │ │ ├── resources │ │ └── META-INF │ │ │ └── services │ │ │ └── org.apache.nifi.controller.ControllerService │ │ └── java │ │ └── de │ │ └── fraunhofer │ │ └── fit │ │ └── opcua │ │ ├── KeyStoreLoader.java │ │ ├── TrustStoreLoader.java │ │ └── StandardOPCUAService.java └── pom.xml ├── .gitignore ├── nifi-opcua-processors ├── src │ ├── test │ │ ├── resources │ │ │ ├── subscribeTags.txt │ │ │ ├── tags.txt │ │ │ └── husky_tags.txt │ │ └── java │ │ │ └── de │ │ │ └── fraunhofer │ │ │ └── fit │ │ │ └── processors │ │ │ └── opcua │ │ │ ├── ListOPCNodesTest.java │ │ │ ├── GetOPCDataTest.java │ │ │ ├── SubscribeOPCNodesTest.java │ │ │ └── utils │ │ │ └── RecordAggregatorTest.java │ └── main │ │ ├── resources │ │ └── META-INF │ │ │ └── services │ │ │ └── org.apache.nifi.processor.Processor │ │ └── java │ │ └── de │ │ └── fraunhofer │ │ └── fit │ │ └── processors │ │ └── opcua │ │ ├── utils │ │ └── RecordAggregator.java │ │ ├── ListOPCNodes.java │ │ ├── SubscribeOPCNodes.java │ │ └── GetOPCData.java └── pom.xml ├── Dockerfile ├── docs ├── list-opc-nodes.md ├── get-opc-data.md ├── subscribe-opc-nodes.md └── standard-opc-ua-service.md ├── NOTICE ├── nifi-opcua-service-api ├── pom.xml └── src │ └── main │ └── java │ └── de │ └── fraunhofer │ └── fit │ └── opcua │ └── OPCUAService.java ├── pom.xml ├── nifi-opcua-nar └── pom.xml ├── README.md └── LICENSE /nifi-opcua-service/src/test/resources/client.jks: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linksmart/nifi-opc-ua-bundles/HEAD/nifi-opcua-service/src/test/resources/client.jks -------------------------------------------------------------------------------- /nifi-opcua-service/src/test/resources/trust.jks: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linksmart/nifi-opc-ua-bundles/HEAD/nifi-opcua-service/src/test/resources/trust.jks -------------------------------------------------------------------------------- /nifi-opcua-service/src/test/resources/trust-wrong.jks: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linksmart/nifi-opc-ua-bundles/HEAD/nifi-opcua-service/src/test/resources/trust-wrong.jks -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Eclipse 2 | .classpath 3 | .project 4 | .settings/ 5 | 6 | # Intellij 7 | .idea/ 8 | *.iml 9 | *.iws 10 | 11 | # Mac 12 | .DS_Store 13 | 14 | # Maven 15 | log/ 16 | target/ -------------------------------------------------------------------------------- /nifi-opcua-processors/src/test/resources/subscribeTags.txt: -------------------------------------------------------------------------------- 1 | ns=4;s=S71500/ET200MP-Station_2.PLC_1.Programs.MAS_Storage_unit_2_DB.c_dispatch 2 | ns=4;s=S71500/ET200MP-Station_2.PLC_1.Programs.MAS_Storage_unit_2_DB.Magazine_Unit_4.c_dispatch -------------------------------------------------------------------------------- /nifi-opcua-processors/src/test/resources/tags.txt: -------------------------------------------------------------------------------- 1 | ns=4;s=S71500/ET200MP-Station_2.PLC_1.GlobalVars.I_MAG1_EXT 2 | ns=4;s=S71500/ET200MP-Station_2.PLC_1.GlobalVars.I_MAG1_RET 3 | ns=4;s=S71500/ET200MP-Station_2.PLC_1.GlobalVars.I_MAG2_EXT 4 | ns=4;s=S71500/ET200MP-Station_2.PLC_1.GlobalVars.I_MAG3_RET 5 | ns=4;s=S71500/ET200MP-Station_2.PLC_1.GlobalVars.I_MAG3_EXT 6 | ns=4;s=S71500/ET200MP-Station_2.PLC_1.GlobalVars.I_MAG3_RET -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Build the nar file 2 | 3 | FROM maven:3.5 as builder 4 | 5 | ARG BASE_DIR=/source 6 | 7 | COPY . ${BASE_DIR} 8 | 9 | WORKDIR ${BASE_DIR} 10 | 11 | RUN mvn clean package -DskipTests 12 | 13 | 14 | # ---------------- 15 | # Build a new Nifi image with the newly generated nar file included 16 | 17 | FROM apache/nifi:1.4.0 18 | 19 | ARG BASE_DIR=/source 20 | 21 | ENV NIFI_BASE_DIR /opt/nifi 22 | ENV NIFI_HOME ${NIFI_BASE_DIR}/nifi-1.4.0 23 | 24 | COPY --from=builder ${BASE_DIR}/nifi-opcua-nar/target/*.nar ${NIFI_HOME}/lib/nifi-opcua.nar 25 | 26 | EXPOSE 8080 8443 10000 27 | 28 | USER nifi 29 | 30 | WORKDIR ${NIFI_HOME} 31 | 32 | # Startup NiFi 33 | ENTRYPOINT ["bin/nifi.sh"] 34 | CMD ["run"] 35 | -------------------------------------------------------------------------------- /docs/list-opc-nodes.md: -------------------------------------------------------------------------------- 1 | # ListOPCNodes 2 | 3 | ### Getting started 4 | 5 | This processor list the existing nodes in an OPC-UA server and output the result as the content of a flowfile. 6 | 7 | Before using this processor, you must set up the StandardOPCUAService first. Documentation can be found [here](standard-opc-ua-service.md). 8 | 9 | ### Configuration 10 | 11 | Property Name | Description 12 | ------|----- 13 | OPC UA Service|Specifies the OPC UA Service that can be used to access data 14 | Starting Nodes|From what node should Nifi begin browsing the node tree. Default is the root node. Seperate multiple nodes with a comma (,) 15 | Recursive Depth|Maximum depth from the starting node to read, Default is 0 16 | Print Indentation|Should Nifi add indentation to the output text 17 | Max References Per Node|The number of Reference Descriptions to pull per node query 18 | Print Non Leaf Nodes|Whether or not to print the nodes which are not leaves -------------------------------------------------------------------------------- /nifi-opcua-service/src/main/resources/META-INF/services/org.apache.nifi.controller.ControllerService: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one or more 2 | # contributor license agreements. See the NOTICE file distributed with 3 | # this work for additional information regarding copyright ownership. 4 | # The ASF licenses this file to You under the Apache License, Version 2.0 5 | # (the "License"); you may not use this file except in compliance with 6 | # the License. You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | de.fraunhofer.fit.opcua.StandardOPCUAService -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Nifi OPC-UA Bundle 2 | Copyright 2014-2018 Fraunhofer Institute for Applied Information Technology FIT 3 | 4 | This product includes software developed at 5 | Fraunhofer Institute for Applied Information Technology FIT (http://fit.fraunhofer.de/). 6 | 7 | Nifi OPC-UA Bundle is an independent fork of the nifi-opcua-bundle (https://github.com/hashmapinc/nifi-opcua-bundle) 8 | developed by HashmapInc. under Apache License v2.0. 9 | 10 | Nifi OPC-UA Bundle includes the following dependencies developed by 11 | the corresponding developers and organisations: 12 | 13 | * https://github.com/apache/nifi by Apache Software Foundation (Apache License v2.0) 14 | * https://github.com/eclipse/milo by Eclipse Foundation (Eclipse Public License v1.0) 15 | * https://github.com/junit-team/junit4 by JUnit-Team (Eclipse Public License v1.0) 16 | * https://www.bouncycastle.org by Legion of Bouncy Castle Inc. (Custom License) 17 | * https://github.com/qos-ch/slf4j by Ceki Gulcu (MIT License) -------------------------------------------------------------------------------- /nifi-opcua-processors/src/main/resources/META-INF/services/org.apache.nifi.processor.Processor: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one or more 2 | # contributor license agreements. See the NOTICE file distributed with 3 | # this work for additional information regarding copyright ownership. 4 | # The ASF licenses this file to You under the Apache License, Version 2.0 5 | # (the "License"); you may not use this file except in compliance with 6 | # the License. You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | de.fraunhofer.fit.processors.opcua.ListOPCNodes 16 | de.fraunhofer.fit.processors.opcua.GetOPCData 17 | de.fraunhofer.fit.processors.opcua.SubscribeOPCNodes -------------------------------------------------------------------------------- /docs/get-opc-data.md: -------------------------------------------------------------------------------- 1 | # GetOPCData 2 | 3 | ### Getting started 4 | 5 | This processor get the values of nodes every time it is triggered. 6 | 7 | Before using this processor, you must set up the StandardOPCUAService first. Documentation can be found [here](standard-opc-ua-service.md). 8 | 9 | ### Configuration 10 | 11 | Property Name | Description 12 | ------|----- 13 | OPC UA Service|Specifies the OPC UA Service that can be used to access data 14 | Return Timestamp|Allows to select the source, server, or both timestamps 15 | Tag List Source|Either get the tag list from the flow file, or from a dynamic property 16 | Tag List Location|The location of the tag list file 17 | Exclude Null Value|Return data only for non null values 18 | Null Value String|If removing null values, what string is used for null 19 | Aggregate Records|Whether to aggregate records. If this is set to true, then variable with the same time stamp will be merged into a single line. 20 | 21 | ### Notes 22 | 1. You can control the interval of data collection by setting the `Scheduling/Run Schedule` property. -------------------------------------------------------------------------------- /nifi-opcua-service-api/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 16 | 17 | 4.0.0 18 | 19 | 20 | de.fraunhofer.fit 21 | nifi-opcua-bundle 22 | 1.0 23 | 24 | 25 | nifi-opcua-service-api 26 | jar 27 | 28 | 29 | 30 | org.apache.nifi 31 | nifi-api 32 | provided 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 16 | 17 | 4.0.0 18 | 19 | 20 | 1.7.1 21 | 22 | 23 | 24 | org.apache.nifi 25 | nifi-nar-bundles 26 | 1.7.1 27 | 28 | 29 | de.fraunhofer.fit 30 | nifi-opcua-bundle 31 | 1.0 32 | pom 33 | 34 | 35 | nifi-opcua-processors 36 | nifi-opcua-service 37 | nifi-opcua-service-api 38 | nifi-opcua-nar 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /nifi-opcua-service/src/test/java/de/fraunhofer/fit/opcua/TestProcessor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package de.fraunhofer.fit.opcua; 18 | 19 | import java.util.ArrayList; 20 | import java.util.List; 21 | 22 | import org.apache.nifi.components.PropertyDescriptor; 23 | import org.apache.nifi.processor.AbstractProcessor; 24 | import org.apache.nifi.processor.ProcessContext; 25 | import org.apache.nifi.processor.ProcessSession; 26 | import org.apache.nifi.processor.exception.ProcessException; 27 | 28 | public class TestProcessor extends AbstractProcessor { 29 | 30 | @Override 31 | public void onTrigger(ProcessContext context, ProcessSession session) throws ProcessException { 32 | } 33 | 34 | @Override 35 | protected List getSupportedPropertyDescriptors() { 36 | List propDescs = new ArrayList<>(); 37 | propDescs.add(new PropertyDescriptor.Builder() 38 | .name("OPCUAService test processor") 39 | .description("OPCUAService test processor") 40 | .identifiesControllerService(OPCUAService.class) 41 | .required(true) 42 | .build()); 43 | return propDescs; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /nifi-opcua-service-api/src/main/java/de/fraunhofer/fit/opcua/OPCUAService.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package de.fraunhofer.fit.opcua; 18 | 19 | import org.apache.nifi.annotation.documentation.CapabilityDescription; 20 | import org.apache.nifi.annotation.documentation.Tags; 21 | import org.apache.nifi.controller.ControllerService; 22 | import org.apache.nifi.processor.exception.ProcessException; 23 | 24 | import java.util.List; 25 | import java.util.concurrent.BlockingQueue; 26 | 27 | @Tags({"example"}) 28 | @CapabilityDescription("Example Service API.") 29 | public interface OPCUAService extends ControllerService { 30 | 31 | byte[] getValue(List reqTagNames, String returnTimestamp, boolean excludeNullValue, 32 | String nullValueString) throws ProcessException; 33 | 34 | byte[] getNodes(String printIndent, int maxRecursiveDepth, int maxReferencePerNode, 35 | boolean printNonLeafNode, String rootNodeId) 36 | throws ProcessException; 37 | 38 | String subscribe(List reqTagNames, BlockingQueue queue, 39 | boolean tsChangedNotify, long minPublishInterval) throws ProcessException; 40 | 41 | void unsubscribe(String subscriberUid); 42 | } 43 | -------------------------------------------------------------------------------- /docs/subscribe-opc-nodes.md: -------------------------------------------------------------------------------- 1 | # SubscribeOPCUANodes 2 | 3 | ### Getting started 4 | 5 | This processor subscribe to the nodes given by you in a tag list file and generate new flowfiles, whenever changes are detected in those nodes. 6 | 7 | Before using this processor, you must set up the StandardOPCUAService first. Documentation can be found [here](standard-opc-ua-service.md). 8 | 9 | ### Configuration 10 | 11 | Property Name | Description 12 | ------|----- 13 | OPC UA Service|Specifies the OPC UA Service that can be used to access data 14 | Tag List File Location|The location of the tag list file 15 | Aggregate Records|Whether to aggregate records. If this is set to true, then variable with the same time stamp will be merged into a single line. This is useful for batch-based data. 16 | Notified when Timestamp changed|Whether the data should be collected, when only the timestamp of a variable has changed, but not its value. 17 | Minimum publish interval of subscription notification messages|The minimum publish interval of subscription notification messages. Set this property to a lower value so that rapid change of data can be detected. 18 | 19 | ### Notes 20 | 21 | 1. This program is structure this way, that each time it is trigger, it will examine the incoming message queue. If new messages are present, then they'll be output as flowfiles. You may want to change the `Scheduling/Run Schedule` property, so that this processor does not waste CPU resource checking on the incoming message queue. 22 | 23 | 2. The tag list file should have the following format, using `\n` as the line separator: 24 | ``` 25 | ns=4;i=12345 26 | ns=4;i=23456 27 | ns=4;i=34567 28 | ``` 29 | 30 | 3. If the `Aggregate Record` option is set, the output of the processor may look like this: 31 | ``` 32 | 1552646838,1.0,2.0,3.0 33 | ``` 34 | In the output flowfile, there is a property field called `csvHeader`, which may look like this: 35 | ``` 36 | timestamp,ns=;i=12345,ns=4;i=23456,ns=4;i=34567 37 | ``` 38 | It is now up to you to merge the record and add a header to the merged flowfile. 39 | -------------------------------------------------------------------------------- /nifi-opcua-nar/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 16 | 17 | 4.0.0 18 | 19 | 20 | de.fraunhofer.fit 21 | nifi-opcua-bundle 22 | 1.0 23 | 24 | 25 | nifi-opcua-nar 26 | 1.0 27 | nar 28 | 29 | nifi-opcua 30 | 31 | 32 | true 33 | true 34 | 35 | 36 | 37 | 38 | de.fraunhofer.fit 39 | nifi-opcua-processors 40 | 1.0 41 | 42 | 43 | de.fraunhofer.fit 44 | nifi-opcua-service-api 45 | 1.0 46 | 47 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /nifi-opcua-processors/src/test/resources/husky_tags.txt: -------------------------------------------------------------------------------- 1 | ns=2;s=47.SerialNumber 2 | ns=2;s=47.Description 3 | ns=2;s=47.MachineState 4 | ns=2;s=47.CycleInterruption 5 | ns=2;s=47.CycleCounter 6 | ns=2;s=47.ProcessVariables.Cycle_Time 7 | ns=2;s=47.ProcessVariables.Mold_Closing_Time 8 | ns=2;s=47.ProcessVariables.Tonnage 9 | ns=2;s=47.ProcessVariables.Shot_Size 10 | ns=2;s=47.ProcessVariables.Cushion 11 | ns=2;s=47.ProcessVariables.Shot_Length 12 | ns=2;s=47.ProcessVariables.Fill_time 13 | ns=2;s=47.ProcessVariables.Transition_Position 14 | ns=2;s=47.ProcessVariables.Transition_Pressure 15 | ns=2;s=47.ProcessVariables.Maximum_Fill_Pressure 16 | ns=2;s=47.ProcessVariables.Hold_Pressure_Zone_-_1 17 | ns=2;s=47.ProcessVariables.Hold_Pressure_Zone_-_2 18 | ns=2;s=47.ProcessVariables.Hold_Pressure_Zone_-_3 19 | ns=2;s=47.ProcessVariables.Hold_Pressure_Zone_-_4 20 | ns=2;s=47.ProcessVariables.Hold_Pressure_Zone_-_10 21 | ns=2;s=47.ProcessVariables.Back_Pressure 22 | ns=2;s=47.ProcessVariables.Screw_Run_Time 23 | ns=2;s=47.ProcessVariables.Effective_Colling_Time 24 | ns=2;s=47.ProcessVariables.Mold_Opening_Time 25 | ns=2;s=47.ProcessVariables.Ejector_Forward_Time 26 | ns=2;s=47.ProcessVariables.Oil_Temperature 27 | ns=2;s=47.ProcessVariables.Extruder_Temperature_-_1 28 | ns=2;s=47.ProcessVariables.Extruder_Temperature_-_2 29 | ns=2;s=47.ProcessVariables.Extruder_Temperature_-_3 30 | ns=2;s=47.ProcessVariables.Barrel_Head_Temperature 31 | ns=2;s=47.ProcessVariables.Ejector_Back_Time 32 | ns=2;s=47.ProcessVariables.Ejector_Maximum_Forward_Position 33 | ns=2;s=47.ProcessVariables.Maximum_Cavity_Pressure 34 | ns=2;s=47.ProcessVariables.Cavity_Pressure_At_Transition 35 | ns=2;s=47.ProcessVariables.Mold_Growth 36 | ns=2;s=47.ProcessVariables.Mold_Open_Time 37 | ns=2;s=47.ProcessVariables.Nozzle_Adapter_Temperature 38 | ns=2;s=47.ProcessVariables.Nozzle_Shutoff_Temperature 39 | ns=2;s=47.ProcessVariables.Screw_RPM 40 | ns=2;s=47.ProcessVariables.Hold_Pressure_Zone_-_5 41 | ns=2;s=47.ProcessVariables.Hold_Pressure_Zone_-_6 42 | ns=2;s=47.ProcessVariables.Hold_Pressure_Zone_-_7 43 | ns=2;s=47.ProcessVariables.Hold_Pressure_Zone_-_8 44 | ns=2;s=47.ProcessVariables.Hold_Pressure_Zone_-_9 45 | ns=2;s=47.ProcessVariables.Extruder_Temperature_-_4 46 | ns=2;s=47.ProcessVariables.Cooling_Time 47 | ns=2;s=47.ProcessVariables.Injection_Hold_Time 48 | ns=2;s=47.MachineEvents.Type 49 | ns=2;s=47.MachineEvents.Message 50 | ns=2;s=47.MachineEvents.Timestamp 51 | ns=2;s=47.MachineEvents.CombinedMessage -------------------------------------------------------------------------------- /nifi-opcua-service/src/main/java/de/fraunhofer/fit/opcua/KeyStoreLoader.java: -------------------------------------------------------------------------------- 1 | package de.fraunhofer.fit.opcua; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | 6 | import java.io.FileInputStream; 7 | import java.security.*; 8 | import java.security.cert.X509Certificate; 9 | import java.util.Enumeration; 10 | import java.util.regex.Pattern; 11 | 12 | class KeyStoreLoader { 13 | 14 | private static final Pattern IP_ADDR_PATTERN = Pattern.compile( 15 | "^(([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\.){3}([01]?\\d\\d?|2[0-4]\\d|25[0-5])$"); 16 | 17 | private final Logger logger = LoggerFactory.getLogger(getClass()); 18 | 19 | private X509Certificate clientCertificate; 20 | private KeyPair clientKeyPair; 21 | 22 | KeyStoreLoader load(String ksLocation, char[] password) throws Exception { 23 | 24 | KeyStore keyStore = KeyStore.getInstance("JKS"); 25 | keyStore.load(new FileInputStream(ksLocation), password); 26 | 27 | clientCertificate = null; 28 | clientKeyPair = null; 29 | 30 | Enumeration aliases = keyStore.aliases(); 31 | // Find the first PrivateKeyEntry in the keystore, which has both PrivateKey and Certificate 32 | while(aliases.hasMoreElements()) { 33 | String alias = aliases.nextElement(); 34 | // Key and keystore share the same password 35 | Key clientPrivateKey = keyStore.getKey(alias, password); 36 | if (clientPrivateKey != null) { 37 | if (clientPrivateKey instanceof PrivateKey) { 38 | if (keyStore.getCertificate(alias) != null) { 39 | clientCertificate = (X509Certificate) keyStore.getCertificate(alias); 40 | PublicKey clientPublicKey = clientCertificate.getPublicKey(); 41 | clientKeyPair = new KeyPair(clientPublicKey, (PrivateKey) clientPrivateKey); 42 | break; 43 | } 44 | } 45 | } 46 | } 47 | 48 | if (clientCertificate == null || clientKeyPair == null) { 49 | throw new Exception("No keypair found in keystore " + ksLocation); 50 | } 51 | 52 | return this; 53 | } 54 | 55 | X509Certificate getClientCertificate() { 56 | return clientCertificate; 57 | } 58 | 59 | KeyPair getClientKeyPair() { 60 | return clientKeyPair; 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /nifi-opcua-processors/src/test/java/de/fraunhofer/fit/processors/opcua/ListOPCNodesTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package de.fraunhofer.fit.processors.opcua; 18 | 19 | import de.fraunhofer.fit.opcua.StandardOPCUAService; 20 | import org.apache.nifi.reporting.InitializationException; 21 | import org.apache.nifi.util.MockFlowFile; 22 | import org.apache.nifi.util.TestRunner; 23 | import org.apache.nifi.util.TestRunners; 24 | import org.junit.Before; 25 | import org.junit.Test; 26 | 27 | import java.util.List; 28 | 29 | public class ListOPCNodesTest { 30 | 31 | private TestRunner testRunner; 32 | private final String endpoint = "opc.tcp://10.223.104.20:48010"; 33 | private StandardOPCUAService service; 34 | 35 | @Before 36 | public void init() throws InitializationException { 37 | testRunner = TestRunners.newTestRunner(ListOPCNodes.class); 38 | service = new StandardOPCUAService(); 39 | testRunner.addControllerService("controller", service); 40 | 41 | testRunner.setProperty(service, StandardOPCUAService.ENDPOINT, endpoint); 42 | testRunner.assertValid(service); 43 | 44 | testRunner.enableControllerService(service); 45 | } 46 | 47 | @Test 48 | public void testListingNodes() { 49 | testRunner.setProperty(ListOPCNodes.OPCUA_SERVICE, "controller"); 50 | testRunner.setProperty(ListOPCNodes.MAX_REFERENCE_PER_NODE, "10"); 51 | testRunner.setProperty(ListOPCNodes.PRINT_INDENTATION, "-"); 52 | //testRunner.setProperty(ListOPCNodes.STARTING_NODE, "ns=4;s=S71500/ET200MP-Station_2.PLC_1"); 53 | testRunner.setProperty(ListOPCNodes.RECURSIVE_DEPTH, "4"); 54 | testRunner.setProperty(ListOPCNodes.PRINT_NON_LEAF_NODES, "true"); 55 | 56 | testRunner.run(); 57 | 58 | List results = testRunner.getFlowFilesForRelationship(GetOPCData.SUCCESS); 59 | System.out.println(new String(testRunner.getContentAsByteArray(results.get(0)))); 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /nifi-opcua-service/src/main/java/de/fraunhofer/fit/opcua/TrustStoreLoader.java: -------------------------------------------------------------------------------- 1 | package de.fraunhofer.fit.opcua; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | 6 | import java.io.FileInputStream; 7 | import java.security.KeyStore; 8 | import java.security.cert.X509Certificate; 9 | import java.util.ArrayList; 10 | import java.util.Arrays; 11 | import java.util.Enumeration; 12 | import java.util.List; 13 | 14 | class TrustStoreLoader { 15 | 16 | private final Logger logger = LoggerFactory.getLogger(getClass()); 17 | 18 | private KeyStore keyStore; 19 | 20 | TrustStoreLoader load(String tsLocation, char[] password) throws Exception { 21 | 22 | keyStore = KeyStore.getInstance("JKS"); 23 | keyStore.load(new FileInputStream(tsLocation), password); 24 | 25 | return this; 26 | } 27 | 28 | // Only verify the first certificate, and CA certificate at the end of the chain. Intermediate certificates are not verified 29 | public void verify(List serverCerts) throws Exception { 30 | 31 | // TODO: maybe also need to verify certificate according to the application name, application uri 32 | 33 | if (serverCerts.size() == 0) throw new Exception("No server certificate."); 34 | 35 | // Get a list of certificates from trust store 36 | List trustedCerts = new ArrayList<>(); 37 | Enumeration aliases = keyStore.aliases(); 38 | while (aliases.hasMoreElements()) { 39 | String alias = aliases.nextElement(); 40 | X509Certificate cert = (X509Certificate) keyStore.getCertificate(alias); 41 | if (cert != null) { 42 | trustedCerts.add(cert); 43 | } 44 | } 45 | 46 | // Check the first certificate to see if it is trusted 47 | for(int i=0; i Arrays.equals(cert.getSignature(), c.getSignature())); 53 | if (certTrusted) return; 54 | 55 | // If no match, then we check whether the current server cert is signed by the next server cert up the chain 56 | if (i < serverCerts.size() - 1) { 57 | try { 58 | serverCerts.get(i).verify(serverCerts.get(i + 1).getPublicKey()); 59 | } catch (Exception e) { 60 | throw new Exception("Server certificate chain not valid."); 61 | } 62 | } 63 | 64 | } 65 | 66 | // If the program reaches this point, it means it has checked all server certs along the chain but none matches 67 | throw new Exception("No trusted certificate found in the server certificate chain"); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nifi OPC-UA Bundle 2 | **Note: This project is currently not actively maintained by LinkSmart. Please refer to [Contributing](#contributing) section to know more.** 3 | 4 | This is a bundle of OPC UA controller service and processors for Nifi. The bundle is an improvement built on top of the OPC UA bundle made by [HashmapInc](https://github.com/hashmapinc/nifi-opcua-bundle). 5 | A couple of differences between the new bundle and the `HashmapInc` one: 6 | 7 | 1. The `HashmapInc` is based on [OPC UA-Java Stack](https://github.com/OPCFoundation/UA-Java), which provides more bottom-level APIs. The new bundle is based on [Eclipse Milo](https://github.com/eclipse/milo), which is built on top of [OPC UA-Java Stack](https://github.com/OPCFoundation/UA-Java) but provides more high-level APIs and more advanced functionalities such as *subscription*. 8 | 2. The new bundle adds a `SubscribeOPCNodes` processor, which allows the user to specify a list of OPC tags to subscribe to. The processor will produce flowfiles, when value changes on subscribed tags are detected. 9 | 3. Adds an option to the `GetOPCData` processor so that the user can specify the source of tag list as a local file. The processor will get values of all tags listed in the file from the OPC. 10 | 4. Adds an option to the `ListOPCNodes` processor, so that user may choose to not get nodes which are not leaves of the tree. This could come in handy, since in most cases, only leaf nodes contain value. 11 | 5. More full-fledged security features, including support for signed or signed & encrypt messages, server certificate verification, etc. 12 | 5. Minor tweaks to improve performance as well as to adapt to our use case. 13 | 14 | ## Build Instructions 15 | ### Build and install NAR file manually 16 | Build it with Maven: 17 | ``` 18 | mvn clean install -DskipTests 19 | ``` 20 | Find the built nar file here: 21 | ``` 22 | /nifi-opcua-nar/target/nifi-opcua.nar 23 | ``` 24 | and copy it to the following directory of the running Nifi instance: 25 | ``` 26 | /opt/nifi/nifi-/lib 27 | ``` 28 | Restart Nifi, then you can find the new processors available. 29 | 30 | ### Build with Docker 31 | Another option is to build a Nifi image containing the NAR file directly: 32 | ``` 33 | docker build -t nifi-opc . 34 | ``` 35 | 36 | ## How to use 37 | 38 | To use the processors in this bundle, you have to set up the `StandardOPCUAService` as Nifi controller service first. Detailed guide can be found [here](docs/standard-opc-ua-service.md). 39 | 40 | For the detailed description of each processor, you can find it here: 41 | - [GetOPCData](docs/get-opc-data.md) 42 | - [ListOPCNodes](docs/list-opc-nodes.md) 43 | - [SubscribeOPCNodes](docs/subscribe-opc-nodes.md) 44 | 45 | ## Contributing 46 | Contributions are welcome in terms of documentation, implementations, and technical support. 47 | 48 | Please fork, make your changes, and submit a pull request. For major changes, please open an issue first and discuss it with the other authors. 49 | -------------------------------------------------------------------------------- /nifi-opcua-processors/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 16 | 17 | 4.0.0 18 | 19 | 20 | de.fraunhofer.fit 21 | nifi-opcua-bundle 22 | 1.0 23 | 24 | 25 | nifi-opcua-processors 26 | jar 27 | 28 | 29 | 30 | org.apache.nifi 31 | nifi-api 32 | ${nifi_version} 33 | 34 | 35 | de.fraunhofer.fit 36 | nifi-opcua-service-api 37 | 1.0 38 | 39 | 40 | org.apache.nifi 41 | nifi-utils 42 | ${nifi_version} 43 | 44 | 45 | org.apache.nifi 46 | nifi-mock 47 | test 48 | ${nifi_version} 49 | 50 | 51 | org.slf4j 52 | slf4j-simple 53 | test 54 | 55 | 56 | junit 57 | junit 58 | test 59 | 60 | 61 | de.fraunhofer.fit 62 | nifi-opcua-service 63 | 1.0 64 | 65 | 66 | org.mockito 67 | mockito-core 68 | 2.10.0 69 | test 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /nifi-opcua-service/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 16 | 17 | 4.0.0 18 | 19 | 20 | de.fraunhofer.fit 21 | nifi-opcua-bundle 22 | 1.0 23 | 24 | 25 | nifi-opcua-service 26 | jar 27 | 28 | 29 | 30 | org.apache.nifi 31 | nifi-utils 32 | ${nifi_version} 33 | 34 | 35 | de.fraunhofer.fit 36 | nifi-opcua-service-api 37 | 1.0 38 | 39 | 40 | org.apache.nifi 41 | nifi-api 42 | provided 43 | 44 | 45 | org.apache.nifi 46 | nifi-processor-utils 47 | ${nifi_version} 48 | 49 | 50 | org.eclipse.milo 51 | sdk-client 52 | 0.2.1 53 | 54 | 55 | 56 | org.bouncycastle 57 | bcprov-jdk15on 58 | 1.58 59 | 60 | 61 | org.bouncycastle 62 | bcpkix-jdk15on 63 | 1.58 64 | 65 | 66 | 67 | org.apache.nifi 68 | nifi-mock 69 | test 70 | ${nifi_version} 71 | 72 | 73 | org.slf4j 74 | slf4j-simple 75 | test 76 | 77 | 78 | junit 79 | junit 80 | test 81 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /nifi-opcua-processors/src/main/java/de/fraunhofer/fit/processors/opcua/utils/RecordAggregator.java: -------------------------------------------------------------------------------- 1 | package de.fraunhofer.fit.processors.opcua.utils; 2 | 3 | import java.util.*; 4 | 5 | public class RecordAggregator { 6 | 7 | // This variable indicates how long a record waits for notification messages from OPC server 8 | private final int PUBLISH_INTERVAL_MULTIPLIER = 4; 9 | 10 | // Index of elements in the queue message 11 | private final int VARIABLE_ID_INDEX = 0; 12 | private final int SOURCE_TS_INDEX = 2; 13 | private final int VALUE_INDEX = 3; 14 | private final int STATUS_CODE_INDEX = 4; 15 | 16 | private long PUBLISH_THRESHOLD_TIME; 17 | private List tags; 18 | private Map tagOrderMap; 19 | private Map recordMap; 20 | 21 | // minPublishInterval is the minimum subscription notification publish interval from OPC UA server 22 | public RecordAggregator(List tags, long minPublishInterval) { 23 | 24 | this.tags = tags; 25 | this.tagOrderMap = new HashMap<>(); 26 | this.recordMap = new HashMap<>(); 27 | 28 | PUBLISH_THRESHOLD_TIME = PUBLISH_INTERVAL_MULTIPLIER * minPublishInterval; 29 | 30 | // Create a map which with tag name as key and its order in list as value 31 | for (int i = 0; i < tags.size(); i++) { 32 | this.tagOrderMap.put(tags.get(i), i); 33 | } 34 | 35 | } 36 | 37 | public void aggregate(String rawMsg) { 38 | 39 | 40 | // msg has the following elements in order: 41 | // 1. variable ID; 2. server time stamp; 3. source time stamp; 4. value; 5. status code 42 | String[] msg = rawMsg.trim().split(","); 43 | 44 | // Ditch all messages with bad status code 45 | if (!msg[STATUS_CODE_INDEX].equals("0")) { 46 | return; 47 | } 48 | 49 | String timeStamp = msg[SOURCE_TS_INDEX]; 50 | String variableId = msg[VARIABLE_ID_INDEX]; 51 | String value = msg[VALUE_INDEX]; 52 | 53 | // Check if a record is already exist for the given time stamp 54 | Record rec; 55 | if (recordMap.containsKey(timeStamp)) { 56 | // Get the record with the same time stamp as the message 57 | rec = recordMap.get(timeStamp); 58 | } else { 59 | // Create a record 60 | rec = new Record(timeStamp, tags.size()); 61 | // Insert it into the map 62 | recordMap.put(timeStamp, rec); 63 | } 64 | // Get the index of the variable given in the message 65 | if (!tagOrderMap.containsKey(variableId)) { 66 | return; 67 | } 68 | int index = tagOrderMap.get(variableId); 69 | // Update the value in the record array 70 | rec.getRecordArray()[index] = value; 71 | } 72 | 73 | 74 | public List getReadyRecords() { 75 | 76 | List list = new ArrayList<>(); 77 | List recordKeyList = new ArrayList<>(recordMap.keySet()); 78 | Collections.sort(recordKeyList); 79 | 80 | for(String key : recordKeyList) { 81 | Record rec = recordMap.get(key); 82 | if(rec.isReady(PUBLISH_THRESHOLD_TIME)) { 83 | list.add(key + "," + 84 | String.join(",", rec.getRecordArray()) + 85 | System.getProperty("line.separator")); 86 | recordMap.remove(key); 87 | } 88 | } 89 | 90 | return list; 91 | } 92 | 93 | 94 | class Record { 95 | 96 | private String timeStamp; 97 | private long createdTime; // The createdTime is used to see whether a record is ready to be published 98 | private String[] recordValues; 99 | 100 | Record(String timeStamp, int recordSize) { 101 | this.timeStamp = timeStamp; 102 | createdTime = System.currentTimeMillis(); 103 | recordValues = new String[recordSize]; 104 | Arrays.fill(recordValues, ""); 105 | } 106 | 107 | String[] getRecordArray() { 108 | return recordValues; 109 | } 110 | 111 | String getTimeStamp() { 112 | return timeStamp; 113 | } 114 | 115 | boolean isReady(long timeThrashold) { 116 | long currentTime = System.currentTimeMillis(); 117 | 118 | return (currentTime - createdTime) > timeThrashold; 119 | } 120 | 121 | } 122 | 123 | } 124 | -------------------------------------------------------------------------------- /docs/standard-opc-ua-service.md: -------------------------------------------------------------------------------- 1 | # StandardOPCUAService 2 | 3 | ### Getting started 4 | 5 | To use other OPC-UA processor, you have to first configure the OPC-UA controller service in Nifi. 6 | The OPC-UA service is responsible for building the connection to the OPC-UA server. 7 | The processors will use the connection established by the service to communicate with the server. 8 | 9 | Detailed guide for setting up controller service in Nifi can be found [here](https://nifi.apache.org/docs/nifi-docs/html/user-guide.html#Controller_Services_for_Dataflows). 10 | 11 | ### Common Configuration 12 | 13 | Property Name | Description 14 | ------|----- 15 | Endpoint URL|The endpoint of the OPC-UA server, e.g. `opc.tcp://192.168.0.2:48010` 16 | Use Proxy|If true, the `Endpoint URL` specified above will be used to establish connection to the server instead of the discovered URL. Useful when connecting to OPC UA server behind NAT or through SSH tunnel, in which the discovered URL is not reachable by the client. 17 | 18 | 19 | ## Security Configuration 20 | The OPC UA controller service provides the possibility for security connection with the OPC server. 21 | In the option `Security Policy`, different security policies could be selected. 22 | If an option other than `None` is chosen, the user must also provide information regarding other security properties. 23 | 24 | Property Name | Description 25 | ------|----- 26 | Security Policy | Different algorithms for signing and encrypting messages. If this option is set to `None`, the following options will not be in effect. 27 | Security Mode | What measure is taken to secure data. `Signed`: data are signed to protect integrity; `SignedAndEncrypt`: Signed and encrypt data to protect privacy. 28 | Application URI | The application URI of your OPC-UA CLIENT. It must match the \"URI\" field in \"Subject Alternative Name\" of your client certificate. Typically it has the form of \"urn:aaa:bbb\". However, whether this field is checked depends on the implementation of the server. That means, for some servers, it is not necessary to specify this field. 29 | Client Keystore Location | The location of the keystore file (`JKS` type). Notice that the keystore should have one `PrivateKeyEntry` (private key + certificate). If multiple exist, then the first one will be used. Also notice that the the key password should be the same as the keystore password. 30 | Client Keystore Password | The password of the keystore (the key password should also be the same) 31 | Require Server Authentication | Whether to verify server certificate against the trust store. It is recommended to disable this option for easier testing, but enable it for production usage. 32 | Trust Store Location | The location of the truststore file (`JKS` type). Multiple certificates inside the trust store is possible. 33 | Trust Store Password | The password of the truststore. 34 | Auth Policy | Choose between "Anonymous" or using username-password for authentication. 35 | Username | Only valid when `Auth Policy` is set to `Username`. The username for authentication. 36 | Password | Only valid when `Auth Policy` is set to `Username`. The password for authentication. 37 | 38 | #### How to test security connection 39 | - Generate a client keystore containing a self-signed certificate (notice that you should use the same password for both storepass and keypass): 40 | 41 | ```bash 42 | keytool -genkey -keyalg RSA -alias nifi-client -keystore client.jks -storepass SuperSecret -keypass SuperSecret -validity 360 -keysize 2048 -ext SAN=uri:urn:nifi:opcua 43 | ``` 44 | 45 | - Download the server certificate from the OPC UA server (let's name it `server.der`); 46 | 47 | - Import the certificate into a JKS trust store: 48 | ```bash 49 | keytool -importcert -file server.der -alias opc-ua-server -keystore trust.jks -storepass SuperSecret 50 | ``` 51 | 52 | - Reference these two stores from the `StandardOPCUAService` property fields. The `Application URI` field should match the `uri` field in `SAN` when generating the client certificate. 53 | 54 | #### Side Notes 55 | - The security features have been tested against the in-house `IBH Link UA` server 56 | 57 | - When the client tries to connect to the OPC server for the first time using a self-signed certificate, 58 | the connection will fail with the error `Bad_SecurityChecksFailed`. This is because your client certificate is not trusted by the OPC-UA server yet. At this point, you should go to the web interface of the OPC server and manually trust the certificate. Restart the service and the connection should then succeed. 59 | 60 | - When you get the error `Bad_CertificateUriInvalid`, it is because the OPC-UA server checks the client's `Application URI` in its `Application Description` and it doesn't match the URI in the client certificate. Make sure you fill in the property field `Application URI` correctly. 61 | 62 | - When testing secure connection, there is possibility that you run into the exception `Illegal Key Size`. For solution, please refer to the post [here](https://deveshsharmablogs.wordpress.com/2012/10/09/fixing-java-security-invalidkeyexception-illegal-key-size-exception/). -------------------------------------------------------------------------------- /nifi-opcua-processors/src/test/java/de/fraunhofer/fit/processors/opcua/GetOPCDataTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package de.fraunhofer.fit.processors.opcua; 18 | 19 | import de.fraunhofer.fit.opcua.StandardOPCUAService; 20 | import org.apache.nifi.reporting.InitializationException; 21 | import org.apache.nifi.util.MockFlowFile; 22 | import org.apache.nifi.util.TestRunner; 23 | import org.apache.nifi.util.TestRunners; 24 | import org.junit.After; 25 | import org.junit.Before; 26 | import org.junit.Test; 27 | import org.mockito.Mockito; 28 | 29 | import java.io.File; 30 | import java.util.List; 31 | 32 | import static org.junit.Assert.assertEquals; 33 | import static org.mockito.ArgumentMatchers.any; 34 | import static org.mockito.ArgumentMatchers.anyBoolean; 35 | import static org.mockito.Mockito.spy; 36 | 37 | 38 | public class GetOPCDataTest { 39 | 40 | private TestRunner testRunner; 41 | private StandardOPCUAService service; 42 | 43 | @Before 44 | public void init() throws InitializationException { 45 | testRunner = TestRunners.newTestRunner(GetOPCData.class); 46 | 47 | // Use partial mock 48 | service = spy(new StandardOPCUAService()); 49 | Mockito.doNothing().when(service).onEnabled(any()); 50 | Mockito.doNothing().when(service).shutdown(); 51 | 52 | testRunner.addControllerService("controller", service); 53 | 54 | testRunner.setProperty(service, StandardOPCUAService.ENDPOINT, "dummy endpoint"); 55 | testRunner.assertValid(service); 56 | 57 | testRunner.enableControllerService(service); 58 | } 59 | 60 | @Test 61 | public void testGetData() { 62 | 63 | String tagFilePath = (new File("src/test/resources/tags.txt")).getAbsolutePath(); 64 | 65 | testRunner.setProperty(GetOPCData.OPCUA_SERVICE, "controller"); 66 | testRunner.setProperty(GetOPCData.RETURN_TIMESTAMP, "Both"); 67 | testRunner.setProperty(GetOPCData.EXCLUDE_NULL_VALUE, "Yes"); 68 | testRunner.setProperty(GetOPCData.TAG_LIST_SOURCE, "Local File"); 69 | testRunner.setProperty(GetOPCData.TAG_LIST_FILE, tagFilePath); 70 | 71 | byte[] values = new String( 72 | "ns=4;s=S71500/ET200MP-Station_2.PLC_1.GlobalVars.I_MAG1_EXT,123456,123456,1,0\n" + 73 | "ns=4;s=S71500/ET200MP-Station_2.PLC_1.GlobalVars.I_MAG1_RET,123456,123456,2,0").getBytes(); 74 | 75 | Mockito.doReturn(values).when(service).getValue(any(), any(), anyBoolean(), any()); 76 | 77 | testRunner.run(); 78 | 79 | List results = testRunner.getFlowFilesForRelationship(GetOPCData.SUCCESS); 80 | assertEquals(1, results.size()); 81 | results.get(0).assertContentEquals("ns=4;s=S71500/ET200MP-Station_2.PLC_1.GlobalVars.I_MAG1_EXT,123456,123456,1,0\n" + 82 | "ns=4;s=S71500/ET200MP-Station_2.PLC_1.GlobalVars.I_MAG1_RET,123456,123456,2,0"); 83 | //System.out.println(new String(testRunner.getContentAsByteArray(results.get(0)))); 84 | 85 | } 86 | 87 | @Test 88 | public void testGetDataWithAggregation() { 89 | 90 | String tagFilePath = (new File("src/test/resources/tags.txt")).getAbsolutePath(); 91 | 92 | testRunner.setProperty(GetOPCData.OPCUA_SERVICE, "controller"); 93 | testRunner.setProperty(GetOPCData.RETURN_TIMESTAMP, "Both"); 94 | testRunner.setProperty(GetOPCData.EXCLUDE_NULL_VALUE, "Yes"); 95 | testRunner.setProperty(GetOPCData.TAG_LIST_SOURCE, "Local File"); 96 | testRunner.setProperty(GetOPCData.TAG_LIST_FILE, tagFilePath); 97 | testRunner.setProperty(GetOPCData.AGGREGATE_RECORD, "true"); 98 | 99 | byte[] values = new String( 100 | "ns=4;s=S71500/ET200MP-Station_2.PLC_1.GlobalVars.I_MAG1_EXT,123456,123456,1,0" + System.lineSeparator() + 101 | "ns=4;s=S71500/ET200MP-Station_2.PLC_1.GlobalVars.I_MAG1_RET,123456,123456,2,0"+ System.lineSeparator() + 102 | "ns=4;s=S71500/ET200MP-Station_2.PLC_1.GlobalVars.I_MAG2_RET,123456,123456,3,0"+ System.lineSeparator() + 103 | "ns=4;s=S71500/ET200MP-Station_2.PLC_1.GlobalVars.I_MAG2_RET,123456,123456,4,0"+ System.lineSeparator() + 104 | "ns=4;s=S71500/ET200MP-Station_2.PLC_1.GlobalVars.I_MAG3_RET,123456,123456,5,0"+ System.lineSeparator() + 105 | "ns=4;s=S71500/ET200MP-Station_2.PLC_1.GlobalVars.I_MAG3_RET,123456,123456,6,0").getBytes(); 106 | 107 | Mockito.doReturn(values).when(service).getValue(any(), any(), anyBoolean(), any()); 108 | 109 | testRunner.run(); 110 | 111 | List results = testRunner.getFlowFilesForRelationship(GetOPCData.SUCCESS); 112 | assertEquals(1, results.size()); 113 | results.get(0).assertContentEquals("123456,1,2,3,4,5,6"); 114 | results.get(0).assertAttributeEquals("csvHeader", 115 | "timestamp," + 116 | "ns=4;s=S71500/ET200MP-Station_2.PLC_1.GlobalVars.I_MAG1_EXT," + 117 | "ns=4;s=S71500/ET200MP-Station_2.PLC_1.GlobalVars.I_MAG1_RET," + 118 | "ns=4;s=S71500/ET200MP-Station_2.PLC_1.GlobalVars.I_MAG2_EXT," + 119 | "ns=4;s=S71500/ET200MP-Station_2.PLC_1.GlobalVars.I_MAG3_RET," + 120 | "ns=4;s=S71500/ET200MP-Station_2.PLC_1.GlobalVars.I_MAG3_EXT," + 121 | "ns=4;s=S71500/ET200MP-Station_2.PLC_1.GlobalVars.I_MAG3_RET"); 122 | 123 | } 124 | 125 | @After 126 | public void shutdown() { 127 | testRunner.disableControllerService(service); 128 | } 129 | 130 | } 131 | -------------------------------------------------------------------------------- /nifi-opcua-service/src/test/java/de/fraunhofer/fit/opcua/TestStandardOPCUAService.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package de.fraunhofer.fit.opcua; 18 | 19 | import org.apache.nifi.reporting.InitializationException; 20 | import org.apache.nifi.util.TestRunner; 21 | import org.apache.nifi.util.TestRunners; 22 | import org.junit.Before; 23 | import org.junit.Test; 24 | 25 | import java.util.Arrays; 26 | import java.util.List; 27 | 28 | public class TestStandardOPCUAService { 29 | 30 | private final String endpoint = "opc.tcp://10.223.104.20:48010"; 31 | private TestRunner runner; 32 | private StandardOPCUAService service; 33 | 34 | @Before 35 | public void init() throws InitializationException { 36 | runner = TestRunners.newTestRunner(TestProcessor.class); 37 | service = new StandardOPCUAService(); 38 | runner.addControllerService("test-good", service); 39 | } 40 | 41 | /* @Test 42 | public void testServiceInitialization() { 43 | 44 | runner.setProperty(service, StandardOPCUAService.ENDPOINT, endpoint); 45 | runner.assertValid(service); 46 | 47 | runner.enableControllerService(service); 48 | 49 | runner.disableControllerService(service); 50 | 51 | } 52 | 53 | @Test 54 | public void testServiceGetNodes() { 55 | runner.setProperty(service, StandardOPCUAService.ENDPOINT, endpoint); 56 | runner.assertValid(service); 57 | 58 | runner.enableControllerService(service); 59 | 60 | System.out.println(new String(service.getNodes("--", 3, 10, false, 61 | "ns=4;s=S71500/ET200MP-Station_2.PLC_1.GlobalVars"))); 62 | 63 | runner.disableControllerService(service); 64 | } 65 | 66 | @Test 67 | public void testServiceGetValues() { 68 | runner.setProperty(service, StandardOPCUAService.ENDPOINT, endpoint); 69 | runner.assertValid(service); 70 | 71 | runner.enableControllerService(service); 72 | 73 | List tagList = Arrays.asList("ns=4;s=S71500/ET200MP-Station_2.PLC_1.GlobalVars.I_MAG1_EXT", 74 | "ns=4;s=S71500/ET200MP-Station_2.PLC_1.GlobalVars.I_MAG2_EXT"); 75 | 76 | byte[] bytes = service.getValue(tagList, "Both", true, ""); 77 | System.out.println(new String(bytes)); 78 | 79 | runner.disableControllerService(service); 80 | } 81 | 82 | @Test 83 | public void testSecurityAccess() { 84 | runner.setProperty(service, StandardOPCUAService.ENDPOINT, endpoint); 85 | runner.setProperty(service, StandardOPCUAService.SECURITY_POLICY, "Basic128Rsa15"); 86 | runner.setProperty(service, StandardOPCUAService.SECURITY_MODE, "SignAndEncrypt"); 87 | runner.setProperty(service, StandardOPCUAService.CLIENT_KS_LOCATION, "src/test/resources/client.jks"); 88 | runner.setProperty(service, StandardOPCUAService.CLIENT_KS_PASSWORD, "password"); 89 | runner.setProperty(service, StandardOPCUAService.REQUIRE_SERVER_AUTH, "true"); 90 | runner.setProperty(service, StandardOPCUAService.TRUSTSTORE_LOCATION, "src/test/resources/trust.jks"); 91 | runner.setProperty(service, StandardOPCUAService.TRUSTSTORE_PASSWORD, "SuperSecret"); 92 | runner.setProperty(service, StandardOPCUAService.AUTH_POLICY, "Anon"); 93 | 94 | runner.assertValid(service); 95 | 96 | runner.enableControllerService(service); 97 | 98 | List tagList = Arrays.asList("ns=4;s=S71500/ET200MP-Station_2.PLC_1.GlobalVars.I_MAG1_EXT", 99 | "ns=4;s=S71500/ET200MP-Station_2.PLC_1.GlobalVars.I_MAG2_EXT"); 100 | 101 | byte[] bytes = service.getValue(tagList, "Both", true, ""); 102 | System.out.println(new String(bytes)); 103 | 104 | runner.disableControllerService(service); 105 | } 106 | 107 | @Test(expected = InitializationException.class) 108 | public void testSecurityAccessWrongTrustStore() throws InitializationException { 109 | runner.setProperty(service, StandardOPCUAService.ENDPOINT, endpoint); 110 | runner.setProperty(service, StandardOPCUAService.SECURITY_POLICY, "Basic128Rsa15"); 111 | runner.setProperty(service, StandardOPCUAService.SECURITY_MODE, "SignAndEncrypt"); 112 | runner.setProperty(service, StandardOPCUAService.CLIENT_KS_LOCATION, "src/test/resources/client.jks"); 113 | runner.setProperty(service, StandardOPCUAService.CLIENT_KS_PASSWORD, "password"); 114 | runner.setProperty(service, StandardOPCUAService.REQUIRE_SERVER_AUTH, "true"); 115 | runner.setProperty(service, StandardOPCUAService.TRUSTSTORE_LOCATION, "src/test/resources/trust-wrong.jks"); 116 | runner.setProperty(service, StandardOPCUAService.TRUSTSTORE_PASSWORD, "password"); 117 | runner.setProperty(service, StandardOPCUAService.AUTH_POLICY, "Anon"); 118 | 119 | runner.assertValid(service); 120 | 121 | try { 122 | runner.enableControllerService(service); 123 | } catch (AssertionError e) { 124 | throw new InitializationException(""); 125 | } 126 | 127 | List tagList = Arrays.asList("ns=4;s=S71500/ET200MP-Station_2.PLC_1.GlobalVars.I_MAG1_EXT", 128 | "ns=4;s=S71500/ET200MP-Station_2.PLC_1.GlobalVars.I_MAG2_EXT"); 129 | 130 | byte[] bytes = service.getValue(tagList, "Both", true, ""); 131 | System.out.println(new String(bytes)); 132 | 133 | runner.disableControllerService(service); 134 | }*/ 135 | 136 | @Test 137 | public void testUsernameSecurityAccess() { 138 | runner.setProperty(service, StandardOPCUAService.ENDPOINT, endpoint); 139 | runner.setProperty(service, StandardOPCUAService.SECURITY_POLICY, "Basic256Sha256"); 140 | runner.setProperty(service, StandardOPCUAService.SECURITY_MODE, "SignAndEncrypt"); 141 | runner.setProperty(service, StandardOPCUAService.APPLICATION_URI, "urn:ibhlinkua_001151:IBHsoftec:IBHLinkUA"); 142 | runner.setProperty(service, StandardOPCUAService.CLIENT_KS_LOCATION, "src/test/resources/client.jks"); 143 | runner.setProperty(service, StandardOPCUAService.CLIENT_KS_PASSWORD, "password"); 144 | runner.setProperty(service, StandardOPCUAService.REQUIRE_SERVER_AUTH, "true"); 145 | runner.setProperty(service, StandardOPCUAService.TRUSTSTORE_LOCATION, "src/test/resources/trust.jks"); 146 | runner.setProperty(service, StandardOPCUAService.TRUSTSTORE_PASSWORD, "SuperSecret"); 147 | runner.setProperty(service, StandardOPCUAService.AUTH_POLICY, "Username"); 148 | runner.setProperty(service, StandardOPCUAService.USERNAME, "test1"); 149 | runner.setProperty(service, StandardOPCUAService.PASSWORD, "password"); 150 | 151 | runner.assertValid(service); 152 | 153 | runner.enableControllerService(service); 154 | 155 | List tagList = Arrays.asList("ns=4;s=S71500/ET200MP-Station_2.PLC_1.GlobalVars.I_MAG1_EXT", 156 | "ns=4;s=S71500/ET200MP-Station_2.PLC_1.GlobalVars.I_MAG2_EXT"); 157 | 158 | byte[] bytes = service.getValue(tagList, "Both", true, ""); 159 | System.out.println(new String(bytes)); 160 | 161 | runner.disableControllerService(service); 162 | } 163 | 164 | } 165 | -------------------------------------------------------------------------------- /nifi-opcua-processors/src/main/java/de/fraunhofer/fit/processors/opcua/ListOPCNodes.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package de.fraunhofer.fit.processors.opcua; 18 | 19 | import de.fraunhofer.fit.opcua.OPCUAService; 20 | import org.apache.nifi.components.PropertyDescriptor; 21 | import org.apache.nifi.components.Validator; 22 | import org.apache.nifi.flowfile.FlowFile; 23 | import org.apache.nifi.annotation.behavior.ReadsAttribute; 24 | import org.apache.nifi.annotation.behavior.ReadsAttributes; 25 | import org.apache.nifi.annotation.behavior.WritesAttribute; 26 | import org.apache.nifi.annotation.behavior.WritesAttributes; 27 | import org.apache.nifi.annotation.lifecycle.OnScheduled; 28 | import org.apache.nifi.annotation.documentation.CapabilityDescription; 29 | import org.apache.nifi.annotation.documentation.SeeAlso; 30 | import org.apache.nifi.annotation.documentation.Tags; 31 | import org.apache.nifi.processor.exception.ProcessException; 32 | import org.apache.nifi.processor.AbstractProcessor; 33 | import org.apache.nifi.processor.ProcessContext; 34 | import org.apache.nifi.processor.ProcessSession; 35 | import org.apache.nifi.processor.ProcessorInitializationContext; 36 | import org.apache.nifi.processor.Relationship; 37 | import org.apache.nifi.processor.util.StandardValidators; 38 | 39 | import java.io.OutputStream; 40 | import java.util.ArrayList; 41 | import java.util.Collections; 42 | import java.util.HashSet; 43 | import java.util.List; 44 | import java.util.Set; 45 | 46 | @Tags({"example"}) 47 | @CapabilityDescription("Provide a description") 48 | @SeeAlso({}) 49 | @ReadsAttributes({@ReadsAttribute(attribute = "", description = "")}) 50 | @WritesAttributes({@WritesAttribute(attribute = "", description = "")}) 51 | public class ListOPCNodes extends AbstractProcessor { 52 | 53 | private static String starting_node = null; 54 | private static String print_indentation = "No"; 55 | private static Integer max_recursiveDepth; 56 | private static Integer max_reference_per_node; 57 | private static boolean print_non_leaf_nodes; 58 | 59 | public static final PropertyDescriptor OPCUA_SERVICE = new PropertyDescriptor.Builder() 60 | .name("OPC UA Service") 61 | .description("Specifies the OPC UA Service that can be used to access data") 62 | .required(true) 63 | .identifiesControllerService(OPCUAService.class) 64 | .build(); 65 | 66 | public static final PropertyDescriptor STARTING_NODE = new PropertyDescriptor 67 | .Builder().name("Starting Nodes") 68 | .description("From what node should Nifi begin browsing the node tree. Default is the root node. Seperate multiple nodes with a comma (,)") 69 | .addValidator(StandardValidators.NON_BLANK_VALIDATOR) 70 | .build(); 71 | 72 | public static final PropertyDescriptor RECURSIVE_DEPTH = new PropertyDescriptor 73 | .Builder().name("Recursive Depth") 74 | .description("Maximum depth from the starting node to read, Default is 0") 75 | .required(true) 76 | .addValidator(StandardValidators.INTEGER_VALIDATOR) 77 | .build(); 78 | 79 | public static final PropertyDescriptor PRINT_INDENTATION = new PropertyDescriptor 80 | .Builder().name("Print Indentation") 81 | .description("Should Nifi add indentation to the output text") 82 | .required(true) 83 | .defaultValue("") 84 | .addValidator(Validator.VALID) 85 | .build(); 86 | 87 | public static final PropertyDescriptor MAX_REFERENCE_PER_NODE = new PropertyDescriptor 88 | .Builder().name("Max References Per Node") 89 | .description("The number of Reference Descriptions to pull per node query.") 90 | .required(true) 91 | .defaultValue("1000") 92 | .addValidator(StandardValidators.POSITIVE_INTEGER_VALIDATOR) 93 | .build(); 94 | 95 | public static final PropertyDescriptor PRINT_NON_LEAF_NODES = new PropertyDescriptor 96 | .Builder().name("Print Non Leaf Nodes") 97 | .description("Whether or not to print the nodes which are not leaves.") 98 | .required(true) 99 | .defaultValue("true") 100 | .addValidator(StandardValidators.BOOLEAN_VALIDATOR) 101 | .build(); 102 | 103 | public static final Relationship SUCCESS = new Relationship.Builder() 104 | .name("Success") 105 | .description("Successful OPC read") 106 | .build(); 107 | 108 | public static final Relationship FAILURE = new Relationship.Builder() 109 | .name("Failure") 110 | .description("Failed OPC read") 111 | .build(); 112 | 113 | public List descriptors; 114 | 115 | public Set relationships; 116 | 117 | @Override 118 | protected void init(final ProcessorInitializationContext context) { 119 | final List descriptors = new ArrayList<>(); 120 | descriptors.add(OPCUA_SERVICE); 121 | descriptors.add(RECURSIVE_DEPTH); 122 | descriptors.add(STARTING_NODE); 123 | descriptors.add(PRINT_INDENTATION); 124 | descriptors.add(MAX_REFERENCE_PER_NODE); 125 | descriptors.add(PRINT_NON_LEAF_NODES); 126 | 127 | this.descriptors = Collections.unmodifiableList(descriptors); 128 | 129 | final Set relationships = new HashSet<>(); 130 | relationships.add(SUCCESS); 131 | relationships.add(FAILURE); 132 | this.relationships = Collections.unmodifiableSet(relationships); 133 | } 134 | 135 | @Override 136 | public Set getRelationships() { 137 | return this.relationships; 138 | } 139 | 140 | @Override 141 | public final List getSupportedPropertyDescriptors() { 142 | return descriptors; 143 | } 144 | 145 | @OnScheduled 146 | public void onScheduled(final ProcessContext context) { 147 | print_indentation = context.getProperty(PRINT_INDENTATION).getValue(); 148 | max_recursiveDepth = Integer.valueOf(context.getProperty(RECURSIVE_DEPTH).getValue()); 149 | starting_node = context.getProperty(STARTING_NODE).getValue(); 150 | max_reference_per_node = Integer.valueOf(context.getProperty(MAX_REFERENCE_PER_NODE).getValue()); 151 | print_non_leaf_nodes = Boolean.valueOf(context.getProperty(PRINT_NON_LEAF_NODES).getValue()); 152 | } 153 | 154 | @Override 155 | public void onTrigger(final ProcessContext context, final ProcessSession session) throws ProcessException { 156 | 157 | final OPCUAService opcUAService = context.getProperty(OPCUA_SERVICE) 158 | .asControllerService(OPCUAService.class); 159 | 160 | byte[] nodes = opcUAService.getNodes(print_indentation, max_recursiveDepth, 161 | max_reference_per_node, print_non_leaf_nodes, starting_node); 162 | 163 | // Write the results back out to a flow file 164 | FlowFile flowFile = session.create(); 165 | 166 | if (flowFile != null) { 167 | try { 168 | flowFile = session.write(flowFile, (OutputStream out) -> { 169 | out.write(nodes); 170 | }); 171 | 172 | // Transfer data to flow file 173 | session.transfer(flowFile, SUCCESS); 174 | } catch (ProcessException ex) { 175 | getLogger().error("Unable to process", ex); 176 | session.transfer(flowFile, FAILURE); 177 | } 178 | } 179 | 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /nifi-opcua-processors/src/test/java/de/fraunhofer/fit/processors/opcua/SubscribeOPCNodesTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package de.fraunhofer.fit.processors.opcua; 18 | 19 | import de.fraunhofer.fit.opcua.StandardOPCUAService; 20 | import org.apache.nifi.reporting.InitializationException; 21 | import org.apache.nifi.util.MockFlowFile; 22 | import org.apache.nifi.util.TestRunner; 23 | import org.apache.nifi.util.TestRunners; 24 | import org.junit.After; 25 | import org.junit.Before; 26 | import org.junit.Test; 27 | import org.mockito.Mockito; 28 | import org.mockito.stubbing.Answer; 29 | 30 | import java.io.File; 31 | import java.util.List; 32 | import java.util.concurrent.BlockingQueue; 33 | 34 | import static org.junit.Assert.assertEquals; 35 | import static org.mockito.ArgumentMatchers.*; 36 | import static org.mockito.Mockito.spy; 37 | 38 | 39 | public class SubscribeOPCNodesTest { 40 | 41 | private TestRunner testRunner; 42 | private StandardOPCUAService service; 43 | 44 | @Before 45 | public void init() throws InitializationException { 46 | testRunner = TestRunners.newTestRunner(SubscribeOPCNodes.class); 47 | 48 | // Use partial mock 49 | service = spy(new StandardOPCUAService()); 50 | Mockito.doNothing().when(service).unsubscribe(anyString()); 51 | Mockito.doNothing().when(service).onEnabled(any()); 52 | Mockito.doNothing().when(service).shutdown(); 53 | 54 | testRunner.addControllerService("controller", service); 55 | 56 | testRunner.setProperty(service, StandardOPCUAService.ENDPOINT, "dummy endpoint"); 57 | testRunner.assertValid(service); 58 | 59 | testRunner.enableControllerService(service); 60 | } 61 | 62 | 63 | @Test 64 | public void testAggregateRecords() throws Exception { 65 | 66 | String tagFilePath = (new File("src/test/resources/husky_tags.txt")).getAbsolutePath(); 67 | 68 | String queueString = 69 | "ns=2;s=47.ProcessVariables.Shot_Length,1528285608582,1528285608582,38.71,0\n" + 70 | "ns=2;s=47.ProcessVariables.Shot_Size,1528285608582,1528285608582,42.543697,0\n" + 71 | "ns=2;s=47.ProcessVariables.Transition_Position,1528285608582,1528285608582,10.848699,0\n" + 72 | "ns=2;s=47.ProcessVariables.Mold_Open_Time,1528285608582,1528285608582,0.20775,0\n" + 73 | "ns=2;s=47.ProcessVariables.Maximum_Fill_Pressure,1528285608582,1528285608582,33.245487,0\n" + 74 | "ns=2;s=47.CycleCounter,1528285608582,1528285608582,2419756,0\n" + 75 | "ns=2;s=47.ProcessVariables.Tonnage,1528285608582,1528285608582,150.13992,0\n" + 76 | "ns=2;s=47.ProcessVariables.Transition_Pressure,1528285608582,1528285608582,32.43998,0\n" + 77 | "ns=2;s=47.ProcessVariables.Back_Pressure,1528285608582,1528285608582,4.540133,0\n" + 78 | "ns=2;s=47.ProcessVariables.Screw_Run_Time,1528285608582,1528285608582,0.948,0\n" + 79 | "ns=2;s=47.ProcessVariables.Oil_Temperature,1528285608582,1528285608582,50.000004,0\n" + 80 | "ns=2;s=47.ProcessVariables.Ejector_Maximum_Forward_Position,1528285608582,1528285608582,11.0207815,0\n" + 81 | "ns=2;s=47.ProcessVariables.Mold_Growth,1528285608582,1528285608582,-0.07940674,0\n" + 82 | "ns=2;s=47.ProcessVariables.Screw_RPM,1528285608582,1528285608582,350.05255,0"; 83 | 84 | 85 | Mockito.doAnswer( 86 | (Answer) invocation -> { 87 | Object[] args = invocation.getArguments(); 88 | populateQueue((BlockingQueue) args[1], queueString); 89 | return "12345678"; // random subscriber uid, doesn't matter in test 90 | } 91 | ).when(service).subscribe(any(), any(), anyBoolean(), anyLong()); 92 | 93 | 94 | testRunner.setProperty(SubscribeOPCNodes.OPCUA_SERVICE, "controller"); 95 | testRunner.setProperty(SubscribeOPCNodes.TAG_FILE_LOCATION, tagFilePath); 96 | testRunner.setProperty(SubscribeOPCNodes.AGGREGATE_RECORD, "true"); 97 | testRunner.setProperty(SubscribeOPCNodes.MIN_PUBLISH_INTERVAL, "100"); 98 | 99 | testRunner.run(1, false, true); 100 | Thread.sleep(500); 101 | testRunner.run(1, true, false); 102 | 103 | 104 | List results = testRunner.getFlowFilesForRelationship(GetOPCData.SUCCESS); 105 | assertEquals(1, results.size()); 106 | String expectedPayload = "1528285608582,,,,,2419756,,,150.13992,42.543697,,38.71,,10.848699,32.43998,33.245487,,,,,,4.540133,0.948,,,,50.000004,,,,,,11.0207815,,,-0.07940674,0.20775,,,350.05255,,,,,,,,,,,," + System.lineSeparator(); 107 | results.get(0).assertContentEquals(expectedPayload); 108 | 109 | } 110 | 111 | 112 | @Test 113 | public void testReal() throws Exception { 114 | 115 | String tagFilePath = (new File("src/test/resources/arburg_tag.txt")).getAbsolutePath(); 116 | 117 | StandardOPCUAService realService = new StandardOPCUAService(); 118 | 119 | testRunner.addControllerService("real", realService); 120 | testRunner.setProperty(realService, StandardOPCUAService.ENDPOINT, "opc.tcp://localhost:9000"); 121 | testRunner.setProperty(realService, StandardOPCUAService.AUTH_POLICY, "Username"); 122 | testRunner.setProperty(realService, StandardOPCUAService.USERNAME, "host_computer"); 123 | testRunner.setProperty(realService, StandardOPCUAService.PASSWORD, ""); 124 | testRunner.setProperty(realService, StandardOPCUAService.USE_PROXY, "true"); 125 | testRunner.enableControllerService(realService); 126 | 127 | testRunner.setProperty(SubscribeOPCNodes.OPCUA_SERVICE, "real"); 128 | testRunner.setProperty(SubscribeOPCNodes.TAG_FILE_LOCATION, tagFilePath); 129 | testRunner.setProperty(SubscribeOPCNodes.AGGREGATE_RECORD, "true"); 130 | testRunner.setProperty(SubscribeOPCNodes.MIN_PUBLISH_INTERVAL, "1000"); 131 | 132 | testRunner.run(1, false, true); 133 | Thread.sleep(1000); 134 | testRunner.run(1, false, false); 135 | Thread.sleep(1000); 136 | testRunner.run(1, false, false); 137 | Thread.sleep(1000); 138 | testRunner.run(1, false, false); 139 | Thread.sleep(1000); 140 | testRunner.run(1, false, false); 141 | Thread.sleep(1000); 142 | testRunner.run(1, false, false); 143 | Thread.sleep(1000); 144 | testRunner.run(1, false, false); 145 | Thread.sleep(1000); 146 | testRunner.run(1, false, false); 147 | Thread.sleep(1000); 148 | testRunner.run(1, true, false); 149 | 150 | 151 | List results = testRunner.getFlowFilesForRelationship(GetOPCData.SUCCESS); 152 | for(MockFlowFile f : results) { 153 | System.out.println(new String(testRunner.getContentAsByteArray(f))); 154 | } 155 | 156 | assertEquals(1, results.size()); 157 | String expectedPayload = "1528285608582,,,,,2419756,,,150.13992,42.543697,,38.71,,10.848699,32.43998,33.245487,,,,,,4.540133,0.948,,,,50.000004,,,,,,11.0207815,,,-0.07940674,0.20775,,,350.05255,,,,,,,,,,,," + System.lineSeparator(); 158 | results.get(0).assertContentEquals(expectedPayload); 159 | 160 | testRunner.disableControllerService(realService); 161 | 162 | } 163 | 164 | 165 | 166 | @After 167 | public void shutdown() { 168 | testRunner.disableControllerService(service); 169 | } 170 | 171 | 172 | private void populateQueue(BlockingQueue queue, String str) { 173 | String[] msgs = str.split("\n"); 174 | for (int i = 0; i < msgs.length; i++) { 175 | queue.offer(msgs[i] + '\n'); 176 | } 177 | } 178 | 179 | } 180 | -------------------------------------------------------------------------------- /nifi-opcua-processors/src/main/java/de/fraunhofer/fit/processors/opcua/SubscribeOPCNodes.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package de.fraunhofer.fit.processors.opcua; 18 | 19 | import de.fraunhofer.fit.opcua.OPCUAService; 20 | import de.fraunhofer.fit.processors.opcua.utils.RecordAggregator; 21 | import org.apache.nifi.annotation.documentation.CapabilityDescription; 22 | import org.apache.nifi.annotation.documentation.Tags; 23 | import org.apache.nifi.annotation.lifecycle.OnScheduled; 24 | import org.apache.nifi.annotation.lifecycle.OnStopped; 25 | import org.apache.nifi.components.PropertyDescriptor; 26 | import org.apache.nifi.expression.ExpressionLanguageScope; 27 | import org.apache.nifi.flowfile.FlowFile; 28 | import org.apache.nifi.processor.*; 29 | import org.apache.nifi.processor.exception.ProcessException; 30 | import org.apache.nifi.processor.util.StandardValidators; 31 | 32 | import java.io.BufferedReader; 33 | import java.io.IOException; 34 | import java.io.OutputStream; 35 | import java.io.StringReader; 36 | import java.nio.charset.Charset; 37 | import java.nio.file.Files; 38 | import java.nio.file.Path; 39 | import java.nio.file.Paths; 40 | import java.util.*; 41 | import java.util.concurrent.BlockingQueue; 42 | import java.util.concurrent.LinkedBlockingQueue; 43 | import java.util.stream.Collectors; 44 | 45 | @Tags({"opc"}) 46 | @CapabilityDescription("Subscribe to a list of nodes and output flowfiles when changes are detected.") 47 | public class SubscribeOPCNodes extends AbstractProcessor { 48 | 49 | private OPCUAService opcUaService; 50 | private BlockingQueue msgQueue; 51 | private List tagNames; 52 | private String subscriberUid; 53 | private boolean aggregateRecord; 54 | private boolean tsChangedNotify; 55 | private long minPublishInterval; 56 | private RecordAggregator recordAggregator; 57 | 58 | public static final PropertyDescriptor OPCUA_SERVICE = new PropertyDescriptor.Builder() 59 | .name("OPC UA Service") 60 | .description("Specifies the OPC UA Service that can be used to access data") 61 | .required(true) 62 | .identifiesControllerService(OPCUAService.class) 63 | .build(); 64 | 65 | public static final PropertyDescriptor TAG_FILE_LOCATION = new PropertyDescriptor 66 | .Builder().name("Tag List File Location") 67 | .description("The location of the tag list file") 68 | .required(true) 69 | .addValidator(StandardValidators.FILE_EXISTS_VALIDATOR) 70 | .sensitive(false) 71 | .expressionLanguageSupported(ExpressionLanguageScope.VARIABLE_REGISTRY) 72 | .build(); 73 | 74 | public static final PropertyDescriptor AGGREGATE_RECORD = new PropertyDescriptor 75 | .Builder().name("Aggregate Records") 76 | .description("Whether to aggregate records. If this is set to true, then variable with the same time stamp will be merged into a single line. This is useful for batch-based data.") 77 | .required(true) 78 | .defaultValue("false") 79 | .allowableValues("true", "false") 80 | .addValidator(StandardValidators.BOOLEAN_VALIDATOR) 81 | .sensitive(false) 82 | .build(); 83 | 84 | public static final PropertyDescriptor TS_CHANGE_NOTIFY = new PropertyDescriptor 85 | .Builder().name("Notified when Timestamp changed") 86 | .description("Whether the data should be collected, when only the timestamp of a variable has changed, but not its value.") 87 | .required(true) 88 | .defaultValue("true") 89 | .allowableValues("true", "false") 90 | .addValidator(StandardValidators.BOOLEAN_VALIDATOR) 91 | .sensitive(false) 92 | .build(); 93 | 94 | public static final PropertyDescriptor MIN_PUBLISH_INTERVAL = new PropertyDescriptor 95 | .Builder().name("Minimum publish interval of subscription notification messages") 96 | .description("The minimum publish interval of subscription notification messages. Set this property to a lower value so that rapid change of data can be detected.") 97 | .required(true) 98 | .defaultValue("1000") 99 | .addValidator(StandardValidators.POSITIVE_INTEGER_VALIDATOR) 100 | .build(); 101 | 102 | public static final Relationship SUCCESS = new Relationship.Builder() 103 | .name("Success") 104 | .description("Successful OPC read") 105 | .build(); 106 | 107 | public static final Relationship FAILURE = new Relationship.Builder() 108 | .name("Failure") 109 | .description("Failed OPC read") 110 | .build(); 111 | 112 | public List descriptors; 113 | 114 | public Set relationships; 115 | 116 | @Override 117 | protected void init(final ProcessorInitializationContext context) { 118 | final List descriptors = new ArrayList<>(); 119 | descriptors.add(OPCUA_SERVICE); 120 | descriptors.add(TAG_FILE_LOCATION); 121 | descriptors.add(AGGREGATE_RECORD); 122 | descriptors.add(TS_CHANGE_NOTIFY); 123 | descriptors.add(MIN_PUBLISH_INTERVAL); 124 | 125 | this.descriptors = Collections.unmodifiableList(descriptors); 126 | 127 | final Set relationships = new HashSet<>(); 128 | relationships.add(SUCCESS); 129 | relationships.add(FAILURE); 130 | this.relationships = Collections.unmodifiableSet(relationships); 131 | 132 | msgQueue = new LinkedBlockingQueue<>(); 133 | } 134 | 135 | @Override 136 | public Set getRelationships() { 137 | return this.relationships; 138 | } 139 | 140 | @Override 141 | public final List getSupportedPropertyDescriptors() { 142 | return descriptors; 143 | } 144 | 145 | @OnScheduled 146 | public void onScheduled(final ProcessContext context) { 147 | 148 | opcUaService = context.getProperty(OPCUA_SERVICE) 149 | .asControllerService(OPCUAService.class); 150 | 151 | 152 | try { 153 | tagNames = parseFile(Paths.get(context.getProperty(TAG_FILE_LOCATION).evaluateAttributeExpressions().toString())); 154 | } catch (IOException e) { 155 | getLogger().error("Error reading tag list from local file."); 156 | return; 157 | } 158 | 159 | aggregateRecord = Boolean.valueOf(context.getProperty(AGGREGATE_RECORD).getValue()); 160 | tsChangedNotify = Boolean.valueOf(context.getProperty(TS_CHANGE_NOTIFY).getValue()); 161 | minPublishInterval = context.getProperty(MIN_PUBLISH_INTERVAL).asLong(); 162 | 163 | subscriberUid = opcUaService.subscribe(tagNames, msgQueue, tsChangedNotify, minPublishInterval); 164 | 165 | recordAggregator = new RecordAggregator(tagNames, minPublishInterval); 166 | } 167 | 168 | @Override 169 | public void onTrigger(final ProcessContext context, final ProcessSession session) throws ProcessException { 170 | 171 | if(!aggregateRecord) { 172 | String msg; 173 | while ((msg = msgQueue.poll()) != null) { 174 | // Write the results back out to a flow file 175 | FlowFile flowFile = session.create(); 176 | 177 | byte[] outputMsgBytes = msg.getBytes(); 178 | if (flowFile != null) { 179 | try { 180 | flowFile = session.write(flowFile, (OutputStream out) -> out.write(outputMsgBytes)); 181 | 182 | // Transfer data to flow file 183 | session.transfer(flowFile, SUCCESS); 184 | } catch (ProcessException ex) { 185 | getLogger().error("Unable to process", ex); 186 | session.transfer(flowFile, FAILURE); 187 | } 188 | } 189 | } 190 | } else { 191 | String rawMsg; 192 | while ((rawMsg = msgQueue.poll()) != null) { 193 | recordAggregator.aggregate(rawMsg); 194 | } 195 | 196 | List list = recordAggregator.getReadyRecords(); 197 | for(String msg: list) { 198 | // Write the results back out to a flow file 199 | FlowFile flowFile = session.create(); 200 | 201 | byte[] outputMsgBytes = msg.getBytes(); 202 | if (flowFile != null) { 203 | try { 204 | flowFile = session.write(flowFile, (OutputStream out) -> out.write(outputMsgBytes)); 205 | 206 | // add header to attribute (remember to add time stamp colum to the first) 207 | Map attrMap = new HashMap<>(); 208 | attrMap.put("csvHeader", "timestamp," + String.join(",", tagNames) + System.getProperty("line.separator")); 209 | flowFile = session.putAllAttributes(flowFile, attrMap); 210 | 211 | // Transfer data to flow file 212 | session.transfer(flowFile, SUCCESS); 213 | } catch (ProcessException ex) { 214 | getLogger().error("Unable to process", ex); 215 | session.transfer(flowFile, FAILURE); 216 | } 217 | } 218 | } 219 | } 220 | 221 | } 222 | 223 | @OnStopped 224 | public void onStopped(final ProcessContext context) { 225 | 226 | getLogger().debug("Unsubscribing from OPC Server..."); 227 | opcUaService.unsubscribe(subscriberUid); 228 | 229 | } 230 | 231 | private List parseFile(Path filePath) throws IOException { 232 | byte[] encoded; 233 | encoded = Files.readAllBytes(filePath); 234 | String fileContent = new String(encoded, Charset.defaultCharset()); 235 | return new BufferedReader(new StringReader(fileContent)).lines().collect(Collectors.toList()); 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2014-2016 Fraunhofer Institute for Applied Information Technology FIT 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. -------------------------------------------------------------------------------- /nifi-opcua-processors/src/main/java/de/fraunhofer/fit/processors/opcua/GetOPCData.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package de.fraunhofer.fit.processors.opcua; 18 | 19 | import de.fraunhofer.fit.opcua.OPCUAService; 20 | import org.apache.nifi.annotation.documentation.CapabilityDescription; 21 | import org.apache.nifi.annotation.documentation.Tags; 22 | import org.apache.nifi.annotation.lifecycle.OnScheduled; 23 | import org.apache.nifi.components.PropertyDescriptor; 24 | import org.apache.nifi.components.Validator; 25 | import org.apache.nifi.expression.ExpressionLanguageScope; 26 | import org.apache.nifi.flowfile.FlowFile; 27 | import org.apache.nifi.processor.*; 28 | import org.apache.nifi.processor.exception.ProcessException; 29 | import org.apache.nifi.processor.util.StandardValidators; 30 | 31 | import java.io.BufferedReader; 32 | import java.io.IOException; 33 | import java.io.InputStreamReader; 34 | import java.io.StringReader; 35 | import java.nio.charset.Charset; 36 | import java.nio.file.Files; 37 | import java.nio.file.Path; 38 | import java.nio.file.Paths; 39 | import java.util.*; 40 | import java.util.concurrent.atomic.AtomicBoolean; 41 | import java.util.concurrent.atomic.AtomicReference; 42 | import java.util.stream.Collectors; 43 | 44 | @Tags({"opc"}) 45 | @CapabilityDescription("Get the data of specified nodes from a OPC UA server.") 46 | public class GetOPCData extends AbstractProcessor { 47 | 48 | private final AtomicReference timestamp = new AtomicReference<>(); 49 | private final AtomicBoolean excludeNullValue = new AtomicBoolean(); 50 | private String nullValueString = ""; 51 | 52 | private List tagList; 53 | 54 | public static final PropertyDescriptor OPCUA_SERVICE = new PropertyDescriptor.Builder() 55 | .name("OPC UA Service") 56 | .description("Specifies the OPC UA Service that can be used to access data") 57 | .required(true) 58 | .identifiesControllerService(OPCUAService.class) 59 | .sensitive(false) 60 | .build(); 61 | 62 | public static final PropertyDescriptor RETURN_TIMESTAMP = new PropertyDescriptor 63 | .Builder().name("Return Timestamp") 64 | .description("Allows to select the source, server, or both timestamps") 65 | .required(true) 66 | .sensitive(false) 67 | .allowableValues("SourceTimestamp", "ServerTimestamp", "Both") 68 | .addValidator(StandardValidators.NON_EMPTY_VALIDATOR) 69 | .build(); 70 | 71 | public static final PropertyDescriptor TAG_LIST_SOURCE = new PropertyDescriptor 72 | .Builder().name("Tag List Source") 73 | .description("Either get the tag list from the flow file, or from a dynamic property") 74 | .required(true) 75 | .allowableValues("Flowfile", "Local File") 76 | .addValidator(StandardValidators.NON_EMPTY_VALIDATOR) 77 | .sensitive(false) 78 | .build(); 79 | 80 | public static final PropertyDescriptor TAG_LIST_FILE = new PropertyDescriptor 81 | .Builder().name("Tag List Location") 82 | .description("The location of the tag list file") 83 | .required(true) 84 | .addValidator(StandardValidators.FILE_EXISTS_VALIDATOR) 85 | .sensitive(false) 86 | .expressionLanguageSupported(ExpressionLanguageScope.VARIABLE_REGISTRY) 87 | .build(); 88 | 89 | public static final PropertyDescriptor EXCLUDE_NULL_VALUE = new PropertyDescriptor 90 | .Builder().name("Exclude Null Value") 91 | .description("Return data only for non null values") 92 | .required(true) 93 | .sensitive(false) 94 | .allowableValues("No", "Yes") 95 | .defaultValue("No") 96 | .addValidator(StandardValidators.NON_EMPTY_VALIDATOR) 97 | .build(); 98 | 99 | public static final PropertyDescriptor NULL_VALUE_STRING = new PropertyDescriptor 100 | .Builder().name("Null Value String") 101 | .description("If removing null values, what string is used for null") 102 | .required(false) 103 | .sensitive(false) 104 | .addValidator(Validator.VALID) 105 | .build(); 106 | 107 | public static final PropertyDescriptor AGGREGATE_RECORD = new PropertyDescriptor 108 | .Builder().name("Aggregate Records") 109 | .description("Whether to aggregate records. If this is set to true, then variable with the same time stamp will be merged into a single line. This is useful for batch-based data.") 110 | .required(true) 111 | .defaultValue("false") 112 | .allowableValues("true", "false") 113 | .addValidator(StandardValidators.BOOLEAN_VALIDATOR) 114 | .sensitive(false) 115 | .build(); 116 | 117 | public static final Relationship SUCCESS = new Relationship.Builder() 118 | .name("Success") 119 | .description("Successful OPC read") 120 | .build(); 121 | 122 | public static final Relationship FAILURE = new Relationship.Builder() 123 | .name("Failure") 124 | .description("Failed OPC read") 125 | .build(); 126 | 127 | private List descriptors; 128 | 129 | private Set relationships; 130 | 131 | @Override 132 | protected void init(final ProcessorInitializationContext context) { 133 | final List descriptors = new ArrayList<>(); 134 | descriptors.add(OPCUA_SERVICE); 135 | descriptors.add(RETURN_TIMESTAMP); 136 | descriptors.add(EXCLUDE_NULL_VALUE); 137 | descriptors.add(NULL_VALUE_STRING); 138 | descriptors.add(TAG_LIST_SOURCE); 139 | descriptors.add(TAG_LIST_FILE); 140 | descriptors.add(AGGREGATE_RECORD); 141 | this.descriptors = Collections.unmodifiableList(descriptors); 142 | 143 | final Set relationships = new HashSet(); 144 | relationships.add(SUCCESS); 145 | relationships.add(FAILURE); 146 | this.relationships = Collections.unmodifiableSet(relationships); 147 | } 148 | 149 | @Override 150 | public Set getRelationships() { 151 | return this.relationships; 152 | } 153 | 154 | @Override 155 | public final List getSupportedPropertyDescriptors() { 156 | return descriptors; 157 | } 158 | 159 | @OnScheduled 160 | public void onScheduled(final ProcessContext context) { 161 | 162 | timestamp.set(context.getProperty(RETURN_TIMESTAMP).getValue()); 163 | excludeNullValue.set(context.getProperty(EXCLUDE_NULL_VALUE).getValue().equals("Yes")); 164 | if (context.getProperty(NULL_VALUE_STRING).isSet()) { 165 | nullValueString = context.getProperty(NULL_VALUE_STRING).getValue(); 166 | } 167 | 168 | // Now every time onSchedule is triggered, data will be read from file anew 169 | if (context.getProperty(TAG_LIST_SOURCE).toString().equals("Local File")) { 170 | try { 171 | tagList = parseFile(Paths.get(context.getProperty(TAG_LIST_FILE).evaluateAttributeExpressions().toString())); 172 | } catch (IOException e) { 173 | getLogger().error("Error reading tag list from local file."); 174 | } 175 | } 176 | } 177 | 178 | @Override 179 | public void onTrigger(final ProcessContext context, final ProcessSession session) throws ProcessException { 180 | 181 | // Initialize response variable 182 | final AtomicReference> requestedTagnames = new AtomicReference<>(); 183 | // Submit to getValue 184 | OPCUAService opcUAService; 185 | 186 | try { 187 | opcUAService = context.getProperty(OPCUA_SERVICE) 188 | .asControllerService(OPCUAService.class); 189 | } catch (Exception ex) { 190 | getLogger().error(ex.getMessage()); 191 | return; 192 | } 193 | 194 | if (context.getProperty(TAG_LIST_SOURCE).toString().equals("Flowfile")) { 195 | 196 | // get FlowFile 197 | FlowFile flowFile = session.get(); 198 | if (flowFile == null) 199 | return; 200 | 201 | // Read tag name from flow file content 202 | session.read(flowFile, in -> { 203 | try { 204 | // TODO: combine this with parseFile 205 | List tagname = new BufferedReader(new InputStreamReader(in)) 206 | .lines().collect(Collectors.toList()); 207 | 208 | requestedTagnames.set(tagname); 209 | 210 | } catch (Exception e) { 211 | getLogger().error("Failed to read flowfile " + e.getMessage()); 212 | } 213 | }); 214 | 215 | session.remove(flowFile); 216 | } else { 217 | 218 | if(tagList == null) 219 | return; 220 | 221 | try { 222 | requestedTagnames.set(tagList); 223 | } catch (Exception ex) { 224 | getLogger().error(ex.getMessage()); 225 | return; 226 | } 227 | } 228 | 229 | FlowFile flowFile; 230 | flowFile = session.get(); 231 | if (flowFile == null) 232 | flowFile = session.create(); 233 | 234 | byte[] values = opcUAService.getValue(requestedTagnames.get(), timestamp.get(), 235 | excludeNullValue.get(), nullValueString); 236 | 237 | if(context.getProperty(AGGREGATE_RECORD).asBoolean()) { 238 | values = mergeRecord(values).getBytes(); 239 | // add csvHeader attribute to flowfile 240 | Map attrMap = new HashMap<>(); 241 | attrMap.put("csvHeader", "timestamp," + String.join(",", requestedTagnames.get())); 242 | flowFile = session.putAllAttributes(flowFile, attrMap); 243 | } 244 | 245 | byte[] payload = values; 246 | 247 | // Write the results back out to flow file 248 | try { 249 | flowFile = session.write(flowFile, out -> out.write(payload)); 250 | session.transfer(flowFile, SUCCESS); 251 | } catch (ProcessException ex) { 252 | getLogger().error("Unable to process", ex); 253 | session.transfer(flowFile, FAILURE); 254 | } 255 | 256 | } 257 | 258 | 259 | private List parseFile(Path filePath) throws IOException { 260 | byte[] encoded; 261 | encoded = Files.readAllBytes(filePath); 262 | String fileContent = new String(encoded, Charset.defaultCharset()); 263 | return new BufferedReader(new StringReader(fileContent)).lines().collect(Collectors.toList()); 264 | } 265 | 266 | private String mergeRecord(byte[] values) { 267 | 268 | int SOURCE_TS_INDEX; 269 | int VALUE_INDEX; 270 | int STATUS_CODE_INDEX; 271 | 272 | if (!timestamp.get().equals("Both")) { 273 | SOURCE_TS_INDEX = 1; 274 | VALUE_INDEX = 2; 275 | STATUS_CODE_INDEX = 3; 276 | } else { 277 | SOURCE_TS_INDEX = 2; 278 | VALUE_INDEX = 3; 279 | STATUS_CODE_INDEX = 4; 280 | } 281 | 282 | String[] rawMsgs = new String(values).split(System.lineSeparator()); 283 | if(rawMsgs.length == 0) return ""; 284 | 285 | StringBuilder sb = new StringBuilder(); 286 | // Use the source timestamp of the first element as the timestamp 287 | 288 | boolean tsAppended = false; 289 | for(int i=0; i queue; 24 | private List tags; 25 | private RecordAggregator ra; 26 | 27 | 28 | @Before 29 | public void init() throws IOException { 30 | queue = new LinkedBlockingQueue<>(); 31 | String tagFilePath = (new File("src\\test\\resources\\husky_tags.txt")).getAbsolutePath(); 32 | tags = parseFile(Paths.get(tagFilePath)); 33 | ra = new RecordAggregator(tags, 100); 34 | } 35 | 36 | @Test 37 | public void testUniformTimeStampNoWait() { 38 | 39 | String queueString = "ns=2;s=47.ProcessVariables.Shot_Length,1528285608582,1528285608582,38.71,0\n" + 40 | "ns=2;s=47.ProcessVariables.Shot_Size,1528285608582,1528285608582,42.543697,0\n" + 41 | "ns=2;s=47.ProcessVariables.Transition_Position,1528285608582,1528285608582,10.848699,0\n" + 42 | "ns=2;s=47.ProcessVariables.Mold_Open_Time,1528285608582,1528285608582,0.20775,0\n" + 43 | "ns=2;s=47.ProcessVariables.Maximum_Fill_Pressure,1528285608582,1528285608582,33.245487,0\n" + 44 | "ns=2;s=47.CycleCounter,1528285608582,1528285608582,2419756,0\n" + 45 | "ns=2;s=47.ProcessVariables.Tonnage,1528285608582,1528285608582,150.13992,0\n" + 46 | "ns=2;s=47.ProcessVariables.Transition_Pressure,1528285608582,1528285608582,32.43998,0\n" + 47 | "ns=2;s=47.ProcessVariables.Back_Pressure,1528285608582,1528285608582,4.540133,0\n" + 48 | "ns=2;s=47.ProcessVariables.Screw_Run_Time,1528285608582,1528285608582,0.948,0\n" + 49 | "ns=2;s=47.ProcessVariables.Oil_Temperature,1528285608582,1528285608582,50.000004,0\n" + 50 | "ns=2;s=47.ProcessVariables.Ejector_Maximum_Forward_Position,1528285608582,1528285608582,11.0207815,0\n" + 51 | "ns=2;s=47.ProcessVariables.Mold_Growth,1528285608582,1528285608582,-0.07940674,0\n" + 52 | "ns=2;s=47.ProcessVariables.Screw_RPM,1528285608582,1528285608582,350.05255,0"; 53 | 54 | String[] msgs = queueString.split("\n"); 55 | for (int i = 0; i parseFile(Path filePath) throws IOException { 239 | byte[] encoded; 240 | encoded = Files.readAllBytes(filePath); 241 | String fileContent = new String(encoded, Charset.defaultCharset()); 242 | return new BufferedReader(new StringReader(fileContent)).lines().collect(Collectors.toList()); 243 | } 244 | 245 | 246 | } 247 | -------------------------------------------------------------------------------- /nifi-opcua-service/src/main/java/de/fraunhofer/fit/opcua/StandardOPCUAService.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package de.fraunhofer.fit.opcua; 18 | 19 | import org.apache.nifi.annotation.documentation.CapabilityDescription; 20 | import org.apache.nifi.annotation.documentation.Tags; 21 | import org.apache.nifi.annotation.lifecycle.OnDisabled; 22 | import org.apache.nifi.annotation.lifecycle.OnEnabled; 23 | import org.apache.nifi.components.PropertyDescriptor; 24 | import org.apache.nifi.components.Validator; 25 | import org.apache.nifi.controller.AbstractControllerService; 26 | import org.apache.nifi.controller.ConfigurationContext; 27 | import org.apache.nifi.expression.ExpressionLanguageScope; 28 | import org.apache.nifi.processor.exception.ProcessException; 29 | import org.apache.nifi.processor.util.StandardValidators; 30 | import org.apache.nifi.reporting.InitializationException; 31 | import org.eclipse.milo.opcua.sdk.client.OpcUaClient; 32 | import org.eclipse.milo.opcua.sdk.client.api.config.OpcUaClientConfigBuilder; 33 | import org.eclipse.milo.opcua.sdk.client.api.identity.AnonymousProvider; 34 | import org.eclipse.milo.opcua.sdk.client.api.identity.IdentityProvider; 35 | import org.eclipse.milo.opcua.sdk.client.api.identity.UsernameProvider; 36 | import org.eclipse.milo.opcua.sdk.client.api.nodes.Node; 37 | import org.eclipse.milo.opcua.sdk.client.api.subscriptions.UaMonitoredItem; 38 | import org.eclipse.milo.opcua.sdk.client.api.subscriptions.UaSubscription; 39 | import org.eclipse.milo.opcua.sdk.client.api.subscriptions.UaSubscriptionManager; 40 | import org.eclipse.milo.opcua.stack.client.UaTcpStackClient; 41 | import org.eclipse.milo.opcua.stack.core.AttributeId; 42 | import org.eclipse.milo.opcua.stack.core.Identifiers; 43 | import org.eclipse.milo.opcua.stack.core.UaException; 44 | import org.eclipse.milo.opcua.stack.core.security.SecurityPolicy; 45 | import org.eclipse.milo.opcua.stack.core.types.builtin.*; 46 | import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UInteger; 47 | import org.eclipse.milo.opcua.stack.core.types.enumerated.DataChangeTrigger; 48 | import org.eclipse.milo.opcua.stack.core.types.enumerated.MessageSecurityMode; 49 | import org.eclipse.milo.opcua.stack.core.types.enumerated.MonitoringMode; 50 | import org.eclipse.milo.opcua.stack.core.types.enumerated.TimestampsToReturn; 51 | import org.eclipse.milo.opcua.stack.core.types.structured.*; 52 | import org.eclipse.milo.opcua.stack.core.util.CertificateUtil; 53 | 54 | import java.security.cert.X509Certificate; 55 | import java.util.ArrayList; 56 | import java.util.Collections; 57 | import java.util.List; 58 | import java.util.Map; 59 | import java.util.concurrent.BlockingQueue; 60 | import java.util.concurrent.ConcurrentHashMap; 61 | import java.util.concurrent.ExecutionException; 62 | import java.util.concurrent.TimeUnit; 63 | import java.util.concurrent.atomic.AtomicLong; 64 | import java.util.function.BiConsumer; 65 | 66 | import static org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.Unsigned.uint; 67 | 68 | @Tags({"opc"}) 69 | @CapabilityDescription("ControllerService implementation of OPCUAService.") 70 | public class StandardOPCUAService extends AbstractControllerService implements OPCUAService { 71 | 72 | public static final PropertyDescriptor ENDPOINT = new PropertyDescriptor 73 | .Builder().name("Endpoint URL") 74 | .description("The opc.tcp address of the opc ua server, e.g. opc.tcp://192.168.0.2:48010") 75 | .required(true) 76 | .addValidator(StandardValidators.NON_EMPTY_VALIDATOR) 77 | .expressionLanguageSupported(ExpressionLanguageScope.VARIABLE_REGISTRY) 78 | .build(); 79 | 80 | public static final PropertyDescriptor SECURITY_POLICY = new PropertyDescriptor 81 | .Builder().name("Security Policy") 82 | .description("What security policy to use for connection with OPC UA server") 83 | .required(true) 84 | .allowableValues("None", "Basic128Rsa15", "Basic256", "Basic256Sha256", "Aes256_Sha256_RsaPss", "Aes128_Sha256_RsaOaep") 85 | .defaultValue("None") 86 | .addValidator(StandardValidators.NON_EMPTY_VALIDATOR) 87 | .build(); 88 | 89 | public static final PropertyDescriptor SECURITY_MODE = new PropertyDescriptor 90 | .Builder().name("Security Mode") 91 | .description("What security mode to use for connection with OPC UA server. Only valid when \"Security Policy\" isn't \"None\".") 92 | .required(true) 93 | .allowableValues("Sign", "SignAndEncrypt") 94 | .defaultValue("Sign") 95 | .addValidator(StandardValidators.NON_EMPTY_VALIDATOR) 96 | .build(); 97 | 98 | 99 | public static final PropertyDescriptor APPLICATION_URI = new PropertyDescriptor 100 | .Builder().name("Application URI") 101 | .description("The application URI of your OPC-UA client. It must match the \"URI\" field in \"Subject Alternative Name\" of your client certificate. Typically it has the form of \"urn:aaa:bbb\". However, whether this field is checked depends on the implementation of the server. That means, for some servers, it is not necessary to specify this field.") 102 | .expressionLanguageSupported(ExpressionLanguageScope.VARIABLE_REGISTRY) 103 | .addValidator(Validator.VALID) 104 | .build(); 105 | 106 | public static final PropertyDescriptor CLIENT_KS_LOCATION = new PropertyDescriptor 107 | .Builder().name("Client Keystore Location") 108 | .description("The location of the client keystore. Only valid when \"Security Policy\" isn't \"None\". " + 109 | "The keystore should contain only one keypair entry (private key + certificate). " + 110 | "If multiple entries exist, the first one is used. " + 111 | "Besides, the key should have the same password as the keystore.") 112 | .addValidator(StandardValidators.FILE_EXISTS_VALIDATOR) 113 | .expressionLanguageSupported(ExpressionLanguageScope.VARIABLE_REGISTRY) 114 | .build(); 115 | 116 | public static final PropertyDescriptor CLIENT_KS_PASSWORD = new PropertyDescriptor 117 | .Builder().name("Client Keystore Password") 118 | .description("The password for the client keystore") 119 | .addValidator(StandardValidators.NON_EMPTY_VALIDATOR) 120 | .expressionLanguageSupported(ExpressionLanguageScope.VARIABLE_REGISTRY) 121 | .sensitive(true) 122 | .build(); 123 | 124 | public static final PropertyDescriptor REQUIRE_SERVER_AUTH = new PropertyDescriptor 125 | .Builder().name("Require server authentication") 126 | .description("Whether to authenticate server by verifying its certificate against the trust store. It is recommended to disable this option for quick test, but enable it for production.") 127 | .allowableValues("true", "false") 128 | .defaultValue("false") 129 | .addValidator(StandardValidators.BOOLEAN_VALIDATOR) 130 | .build(); 131 | 132 | public static final PropertyDescriptor TRUSTSTORE_LOCATION = new PropertyDescriptor 133 | .Builder().name("Trust store Location") 134 | .description("The location of the trust store. Only valid when \"Security Policy\" isn't \"None\". " + 135 | "Trust store contains trusted certificates, which are to be used for server identity verification." + 136 | "The trust store can contain multiple certificates.") 137 | .addValidator(StandardValidators.FILE_EXISTS_VALIDATOR) 138 | .expressionLanguageSupported(ExpressionLanguageScope.VARIABLE_REGISTRY) 139 | .build(); 140 | 141 | public static final PropertyDescriptor TRUSTSTORE_PASSWORD = new PropertyDescriptor 142 | .Builder().name("Trust store Password") 143 | .description("The password for the trust store") 144 | .addValidator(StandardValidators.NON_EMPTY_VALIDATOR) 145 | .expressionLanguageSupported(ExpressionLanguageScope.VARIABLE_REGISTRY) 146 | .sensitive(true) 147 | .build(); 148 | 149 | public static final PropertyDescriptor AUTH_POLICY = new PropertyDescriptor 150 | .Builder().name("Authentication Policy") 151 | .description("How should Nifi authenticate with the UA server") 152 | .required(true) 153 | .defaultValue("Anon") 154 | .allowableValues("Anon", "Username") 155 | .addValidator(StandardValidators.NON_EMPTY_VALIDATOR) 156 | .build(); 157 | 158 | public static final PropertyDescriptor USERNAME = new PropertyDescriptor 159 | .Builder().name("User Name") 160 | .description("The user name to access the OPC UA server (only valid when \"Authentication Policy\" is \"Username\")") 161 | .required(false) 162 | .addValidator(StandardValidators.NON_EMPTY_VALIDATOR) 163 | .expressionLanguageSupported(ExpressionLanguageScope.VARIABLE_REGISTRY) 164 | .build(); 165 | 166 | public static final PropertyDescriptor PASSWORD = new PropertyDescriptor 167 | .Builder().name("Password") 168 | .description("The password to access the OPC UA server (only valid when \"Authentication Policy\" is \"Username\")") 169 | .required(false) 170 | .sensitive(true) 171 | .addValidator(Validator.VALID) 172 | .expressionLanguageSupported(ExpressionLanguageScope.VARIABLE_REGISTRY) 173 | .build(); 174 | 175 | public static final PropertyDescriptor USE_PROXY = new PropertyDescriptor 176 | .Builder().name("Use Proxy") 177 | .description("If true, the \"Endpoint URL\" specified above will be used to establish connection to the server instead of the discovered URL. " + 178 | "Useful when connecting to OPC UA server behind NAT or through SSH tunnel, in which the discovered URL is not reachable by the client.") 179 | .required(true) 180 | .defaultValue("false") 181 | .addValidator(StandardValidators.BOOLEAN_VALIDATOR) 182 | .expressionLanguageSupported(ExpressionLanguageScope.VARIABLE_REGISTRY) 183 | .build(); 184 | 185 | private static final List properties; 186 | 187 | private OpcUaClient opcClient; 188 | private Map subscriptionMap; 189 | 190 | private final AtomicLong clientHandles = new AtomicLong(1L); 191 | 192 | static { 193 | final List props = new ArrayList<>(); 194 | props.add(ENDPOINT); 195 | props.add(SECURITY_POLICY); 196 | props.add(SECURITY_MODE); 197 | props.add(APPLICATION_URI); 198 | props.add(CLIENT_KS_LOCATION); 199 | props.add(CLIENT_KS_PASSWORD); 200 | props.add(REQUIRE_SERVER_AUTH); 201 | props.add(TRUSTSTORE_LOCATION); 202 | props.add(TRUSTSTORE_PASSWORD); 203 | props.add(AUTH_POLICY); 204 | props.add(USERNAME); 205 | props.add(PASSWORD); 206 | props.add(USE_PROXY); 207 | properties = Collections.unmodifiableList(props); 208 | } 209 | 210 | @Override 211 | protected List getSupportedPropertyDescriptors() { 212 | return properties; 213 | } 214 | 215 | /** 216 | * @param context the configuration context 217 | * @throws InitializationException exceptions that happens during the connection establishing phase 218 | */ 219 | @OnEnabled 220 | public void onEnabled(final ConfigurationContext context) throws InitializationException { 221 | 222 | String endpoint = context.getProperty(ENDPOINT).evaluateAttributeExpressions().getValue(); 223 | if (endpoint == null) { 224 | throw new InitializationException("Endpoint can't be null."); 225 | } 226 | 227 | // Get the Security Mode 228 | MessageSecurityMode minSecurityMode; 229 | if (context.getProperty(SECURITY_POLICY).getValue().equals("None")) { 230 | minSecurityMode = MessageSecurityMode.None; 231 | } else if (context.getProperty(SECURITY_MODE).getValue().equals("Sign")) { 232 | minSecurityMode = MessageSecurityMode.Sign; 233 | } else { 234 | minSecurityMode = MessageSecurityMode.SignAndEncrypt; 235 | } 236 | 237 | // Get the security policy 238 | SecurityPolicy minSecurityPolicy; 239 | switch(context.getProperty(SECURITY_POLICY).getValue()) { 240 | case "Basic128Rsa15": 241 | minSecurityPolicy = SecurityPolicy.Basic128Rsa15; 242 | break; 243 | case "Basic256": 244 | minSecurityPolicy = SecurityPolicy.Basic256; 245 | break; 246 | case "Basic256Sha256": 247 | minSecurityPolicy = SecurityPolicy.Basic256Sha256; 248 | break; 249 | case "Aes256_Sha256_RsaPss": 250 | minSecurityPolicy = SecurityPolicy.Aes256_Sha256_RsaPss; 251 | break; 252 | case "Aes128_Sha256_RsaOaep": 253 | minSecurityPolicy = SecurityPolicy.Aes128_Sha256_RsaOaep; 254 | break; 255 | default: 256 | minSecurityPolicy = SecurityPolicy.None; 257 | minSecurityMode = MessageSecurityMode.None; 258 | break; 259 | } 260 | 261 | try { 262 | EndpointDescription[] endpoints = 263 | UaTcpStackClient.getEndpoints(endpoint).get(); 264 | 265 | EndpointDescription endpointDescription = chooseEndpoint(endpoints, minSecurityPolicy, minSecurityMode); 266 | 267 | if (endpointDescription == null) { 268 | StringBuilder sb = new StringBuilder(); 269 | sb.append(String.format("No exact security configuration match is found. \n" + 270 | "You specified security mode: %s, security policy: %s\n" + 271 | "Available combinations: \n", 272 | minSecurityMode.name(), 273 | minSecurityPolicy.getSecurityPolicyUri())); 274 | 275 | for(EndpointDescription ed : endpoints) { 276 | sb.append(String.format("security mode: %s, security policy: %s\n", 277 | ed.getSecurityMode().name(), 278 | ed.getSecurityPolicyUri())); 279 | } 280 | throw new InitializationException(sb.toString()); 281 | } 282 | 283 | OpcUaClientConfigBuilder cfgBuilder = new OpcUaClientConfigBuilder(); 284 | 285 | // The following code is used to force the client to connect to the URL given by user, 286 | // instead of using the discovered URL. Useful when client is visiting the server through 287 | // some NAT or SSH tunneling, and the discovered URL is not reachable. 288 | if (context.getProperty(USE_PROXY).asBoolean()) { 289 | endpointDescription = new EndpointDescription(endpoint, 290 | endpointDescription.getServer(), 291 | endpointDescription.getServerCertificate(), 292 | endpointDescription.getSecurityMode(), 293 | endpointDescription.getSecurityPolicyUri(), 294 | endpointDescription.getUserIdentityTokens(), 295 | endpointDescription.getTransportProfileUri(), 296 | endpointDescription.getSecurityLevel()); 297 | } 298 | 299 | cfgBuilder.setEndpoint(endpointDescription); 300 | if (!context.getProperty(SECURITY_POLICY).getValue().equals("None")) { // If security policy is used 301 | 302 | // clientKsLocation has already been validated, no need to check again 303 | String clientKsLocation = context.getProperty(CLIENT_KS_LOCATION).evaluateAttributeExpressions().getValue(); 304 | char[] clientKsPassword = context.getProperty(CLIENT_KS_PASSWORD).evaluateAttributeExpressions().getValue() != null ? 305 | context.getProperty(CLIENT_KS_PASSWORD).evaluateAttributeExpressions().getValue().toCharArray() : null; 306 | 307 | // Verify server certificate against the trust store 308 | if (context.getProperty(REQUIRE_SERVER_AUTH).asBoolean()) { 309 | 310 | // trustStoreLocation has already been validated, no need to check again 311 | String trustStoreLocation = context.getProperty(TRUSTSTORE_LOCATION).evaluateAttributeExpressions().getValue(); 312 | char[] trustStorePassword = context.getProperty(TRUSTSTORE_PASSWORD).evaluateAttributeExpressions().getValue() != null ? 313 | context.getProperty(TRUSTSTORE_PASSWORD).evaluateAttributeExpressions().getValue().toCharArray() : null; 314 | 315 | TrustStoreLoader tsLoader = new TrustStoreLoader().load(trustStoreLocation, trustStorePassword); 316 | List serverCerts = CertificateUtil.decodeCertificates( 317 | endpointDescription.getServerCertificate().bytes()); 318 | 319 | try { 320 | // Only verify the first certificate, and CA certificate at the end of the chain. Intermediate certificates are not verified 321 | tsLoader.verify(serverCerts); 322 | } catch (Exception e) { 323 | getLogger().error("Cannot verify server certificate. Cause: " + e.getMessage() 324 | + " Please make sure you have added the server certificate to the trust store."); 325 | throw new InitializationException(e.getMessage()); 326 | } 327 | } 328 | 329 | KeyStoreLoader loader = new KeyStoreLoader().load(clientKsLocation, clientKsPassword); 330 | 331 | cfgBuilder.setCertificate(loader.getClientCertificate()) 332 | .setKeyPair(loader.getClientKeyPair()) 333 | .setRequestTimeout(uint(5000)); 334 | } 335 | 336 | String authType = context.getProperty(AUTH_POLICY).getValue(); 337 | IdentityProvider identityProvider; 338 | if (authType.equals("Anon")) { 339 | identityProvider = new AnonymousProvider(); 340 | } else { 341 | String username = context.getProperty(USERNAME).evaluateAttributeExpressions().getValue(); 342 | String password = context.getProperty(PASSWORD).evaluateAttributeExpressions().getValue(); 343 | identityProvider = new UsernameProvider(username == null ? "" : username, 344 | password == null ? "" : password); 345 | } 346 | 347 | cfgBuilder.setIdentityProvider(identityProvider); 348 | 349 | String applicationUri = context.getProperty(APPLICATION_URI).evaluateAttributeExpressions().getValue(); 350 | if(applicationUri != null) { 351 | cfgBuilder.setApplicationUri(applicationUri); 352 | cfgBuilder.setProductUri(applicationUri); 353 | } 354 | 355 | opcClient = new OpcUaClient(cfgBuilder.build()); 356 | opcClient.connect().get(5, TimeUnit.SECONDS); 357 | 358 | if (subscriptionMap == null) { 359 | subscriptionMap = new ConcurrentHashMap<>(); 360 | } 361 | 362 | // Add custom SubscriptionListener to handle automatic recreating subscription 363 | opcClient.getSubscriptionManager().addSubscriptionListener(new CustomSubscriptionListener()); 364 | 365 | } catch (Exception e) { 366 | throw new InitializationException(e); 367 | } 368 | 369 | } 370 | 371 | @OnDisabled 372 | public void shutdown() { 373 | try { 374 | if (opcClient != null) { 375 | getLogger().debug("Disconnecting from OPC server..."); 376 | opcClient.disconnect().get(3, TimeUnit.SECONDS); 377 | } 378 | } catch (Exception e) { 379 | getLogger().warn(e.getMessage()); 380 | } 381 | } 382 | 383 | 384 | /** 385 | * Get the value according to a list of node names. This method uses the Read Attribute Service. 386 | * 387 | * @param tagNames A list of OPC UA node names 388 | * @param returnTimestamp What timestamp to return. "Both", "Source" and "Server" 389 | * @param excludeNullValue If null value in data is encountered, whether exclude them from adding to the final response 390 | * @param nullValueString String to replace the null value, if excludeNullValue is false 391 | * @return A UTF-8 byte array of response 392 | * @throws ProcessException Exceptions happens when getting values from OPC-UA server 393 | */ 394 | @Override 395 | public byte[] getValue(List tagNames, String returnTimestamp, boolean excludeNullValue, 396 | String nullValueString) throws ProcessException { 397 | try { 398 | if (opcClient == null) { 399 | throw new ProcessException("OPC Client is null. OPC UA service was not enabled properly."); 400 | } 401 | 402 | // TODO: Throw more descriptive exception when parsing fails 403 | ArrayList nodeIdList = new ArrayList<>(); 404 | tagNames.forEach((tagName) -> nodeIdList.add(NodeId.parse(tagName))); 405 | 406 | 407 | List rvList = opcClient.readValues(0, TimestampsToReturn.Both, nodeIdList).get(); 408 | 409 | StringBuilder serverResponse = new StringBuilder(); 410 | 411 | for (int i = 0; i < tagNames.size(); i++) { 412 | String valueLine; 413 | valueLine = writeCsv(tagNames.get(i), returnTimestamp, rvList.get(i), excludeNullValue, nullValueString); 414 | serverResponse.append(valueLine); 415 | } 416 | 417 | return serverResponse.toString().trim().getBytes(); 418 | 419 | } catch (Exception e) { 420 | throw new ProcessException(e); 421 | } 422 | 423 | } 424 | 425 | 426 | @Override 427 | public String subscribe(List tagNames, BlockingQueue queue, 428 | boolean tsChangedNotify, long minPublishInterval) throws ProcessException { 429 | 430 | try { 431 | if (opcClient == null) { 432 | throw new Exception("OPC Client is null. OPC UA service was not enabled properly."); 433 | } 434 | 435 | List readValueIds = new ArrayList<>(); 436 | tagNames.forEach((tagName) -> { 437 | ReadValueId readValueId = new ReadValueId( 438 | NodeId.parse(tagName), 439 | AttributeId.Value.uid(), null, QualifiedName.NULL_VALUE); 440 | readValueIds.add(readValueId); 441 | }); 442 | 443 | // Important! 444 | // If we apply this filter in MonitoringParameters, now not only we will get data when value changes, 445 | // we will also get data even value doesn't change, but the timestamp has changed. 446 | // If it is null, then the default DataChangeFilter will be used, which only get data when its value changes. 447 | DataChangeFilter changeFilter = tsChangedNotify ? 448 | new DataChangeFilter(DataChangeTrigger.from(2), null, null) : null; 449 | 450 | UaSubscription sub = createSubscription(minPublishInterval); 451 | 452 | createMonitorItems(sub, readValueIds, queue, changeFilter); 453 | 454 | return putSubToMap(sub, queue); 455 | 456 | } catch (Exception e) { 457 | throw new ProcessException(e.getMessage()); 458 | } 459 | } 460 | 461 | @Override 462 | public void unsubscribe(String subscriptionUid) { 463 | 464 | if (opcClient == null) { 465 | getLogger().warn("OPC Client is null. OPC UA service was not enabled properly."); 466 | return; 467 | } 468 | 469 | if (subscriptionMap.get(subscriptionUid) != null) { 470 | try { 471 | UInteger subId = UInteger.valueOf(subscriptionUid); 472 | opcClient.getSubscriptionManager() 473 | .deleteSubscription(subId).get(4, TimeUnit.SECONDS); 474 | subscriptionMap.remove(subscriptionUid); 475 | } catch (Exception e) { 476 | getLogger().warn("Unsubscribe failed: " + e.getMessage()); 477 | } 478 | } 479 | 480 | } 481 | 482 | @Override 483 | public byte[] getNodes(String indentString, int maxRecursiveDepth, int maxReferencePerNode, 484 | boolean printNonLeafNode, String rootNodeId) 485 | throws ProcessException { 486 | 487 | try { 488 | if (opcClient == null) { 489 | throw new ProcessException("OPC Client is null. OPC UA service was not enabled properly."); 490 | } 491 | 492 | NodeId nodeId; 493 | if (rootNodeId == null || rootNodeId.isEmpty()) { 494 | nodeId = Identifiers.RootFolder; 495 | } else { 496 | nodeId = NodeId.parse(rootNodeId); 497 | } 498 | 499 | StringBuilder builder = new StringBuilder(); 500 | browseNodeIteratively("", indentString, maxRecursiveDepth, maxReferencePerNode, printNonLeafNode, 501 | opcClient, nodeId, builder); 502 | 503 | return builder.toString().getBytes(); 504 | 505 | } catch (Exception e) { 506 | throw new ProcessException(e.getMessage()); 507 | } 508 | 509 | } 510 | 511 | // Choose the proper endpoint from discovered endpoints according to security settings 512 | private EndpointDescription chooseEndpoint( 513 | EndpointDescription[] endpoints, 514 | SecurityPolicy minSecurityPolicy, 515 | MessageSecurityMode minMessageSecurityMode) { 516 | 517 | for (EndpointDescription endpoint : endpoints) { 518 | SecurityPolicy endpointSecurityPolicy; 519 | try { 520 | endpointSecurityPolicy = SecurityPolicy.fromUri(endpoint.getSecurityPolicyUri()); 521 | } catch (UaException e) { 522 | continue; 523 | } 524 | if (minSecurityPolicy.compareTo(endpointSecurityPolicy) == 0 && 525 | minMessageSecurityMode.compareTo(endpoint.getSecurityMode()) == 0) { 526 | // Found endpoint which fulfills minimum requirements 527 | return endpoint; 528 | } 529 | } 530 | return null; 531 | } 532 | 533 | // remainDepth = 0 means only print out the current node 534 | // StringBuilder is passed into the recursive method to reduce generating strings and improve performance 535 | private void browseNodeIteratively(String currentIndent, String indentString, int remainDepth, int maxRefPerNode, 536 | boolean printNonLeafNode, OpcUaClient client, NodeId browseRoot, StringBuilder builder) { 537 | 538 | //getLogger().info(indent + " Node=" + node.getNodeId().get().getIdentifier().toString()); 539 | 540 | try { 541 | List nodes = client.getAddressSpace().browse(browseRoot).get(); 542 | 543 | if (printNonLeafNode || nodes.size() == 0) { 544 | builder.append(currentIndent) 545 | .append(getFullName(browseRoot)) 546 | .append("\n"); 547 | } 548 | 549 | if (remainDepth > 0) { 550 | 551 | String newIndent = currentIndent + indentString; 552 | remainDepth--; 553 | 554 | int currNodeCount = 0; 555 | 556 | for (Node node : nodes) { 557 | if (currNodeCount == maxRefPerNode) 558 | break; 559 | 560 | // recursively browse to children 561 | browseNodeIteratively(newIndent, indentString, remainDepth, maxRefPerNode, printNonLeafNode, 562 | client, node.getNodeId().get(), builder); 563 | 564 | currNodeCount++; 565 | } 566 | } 567 | 568 | } catch (InterruptedException | ExecutionException e) { 569 | getLogger().error("Browsing nodeId=" + browseRoot + " failed: " + e.getMessage()); 570 | } 571 | 572 | } 573 | 574 | private String getFullName(NodeId nodeId) { 575 | 576 | String identifierType; 577 | 578 | switch (nodeId.getType()) { 579 | case Numeric: 580 | identifierType = "i"; 581 | break; 582 | case Opaque: 583 | identifierType = "b"; 584 | break; 585 | case Guid: 586 | identifierType = "g"; 587 | break; 588 | default: 589 | identifierType = "s"; 590 | } 591 | 592 | return String.format("ns=%s;%s=%s", nodeId.getNamespaceIndex().toString(), 593 | identifierType, nodeId.getIdentifier().toString()); 594 | } 595 | 596 | 597 | private UaSubscription createSubscription(long minPublishInterval) throws Exception { 598 | return opcClient.getSubscriptionManager() 599 | .createSubscription((double) minPublishInterval).get(); 600 | } 601 | 602 | 603 | private void createMonitorItems(UaSubscription uaSubscription, List readValueIds, 604 | BlockingQueue queue, DataChangeFilter df) throws Exception { 605 | 606 | // Create a list of MonitoredItemCreateRequest 607 | ArrayList micrList = new ArrayList<>(); 608 | readValueIds.forEach((readValueId) -> { 609 | 610 | Long clientHandleLong = clientHandles.getAndIncrement(); 611 | UInteger clientHandle = uint(clientHandleLong); 612 | 613 | MonitoringParameters parameters = new MonitoringParameters( 614 | clientHandle, 615 | 300.0, // sampling interval 616 | ExtensionObject.encode(df), // filter, null means use default 617 | uint(10), // queue size 618 | true // discard oldest 619 | ); 620 | 621 | micrList.add(new MonitoredItemCreateRequest( 622 | readValueId, MonitoringMode.Reporting, parameters)); 623 | 624 | }); 625 | 626 | // This is the callback when the MonitoredItem is created. In this callback, we set the consumer for incoming values 627 | BiConsumer onItemCreated = 628 | (item, id) -> item.setValueConsumer((it, value) -> { 629 | getLogger().debug("subscription value received: item=" + it.getReadValueId().getNodeId() 630 | + " value=" + value.getValue()); 631 | String valueLine = writeCsv(getFullName(it.getReadValueId().getNodeId()), 632 | "Both", value, false, ""); 633 | 634 | queue.offer(valueLine); 635 | }); 636 | 637 | List items = uaSubscription.createMonitoredItems( 638 | TimestampsToReturn.Both, 639 | micrList, 640 | onItemCreated 641 | ).get(); 642 | 643 | for (UaMonitoredItem item : items) { 644 | if (item.getStatusCode().isGood()) { 645 | getLogger().debug("item created for nodeId=" + item.getReadValueId().getNodeId()); 646 | } else { 647 | getLogger().error("failed to create item for nodeId=" + item.getReadValueId().getNodeId() 648 | + " (status=" + item.getStatusCode() + ")"); 649 | } 650 | } 651 | 652 | } 653 | 654 | // Put SubscriptionConfig to a map for later retrieval 655 | private String putSubToMap(UaSubscription sub, BlockingQueue queue) { 656 | String subUid = sub.getSubscriptionId().toString(); 657 | subscriptionMap.put(subUid, new SubscriptionConfig(sub, queue)); 658 | return subUid; 659 | } 660 | 661 | 662 | private String writeCsv(String tagName, String returnTimestamp, DataValue value, 663 | boolean excludeNullValue, String nullValueString) { 664 | 665 | String sValue = nullValueString; 666 | 667 | if (value == null || value.getValue() == null || value.getValue().getValue() == null) { 668 | 669 | if (excludeNullValue) { 670 | getLogger().debug("Null value returned for " + tagName 671 | + " -- Skipping because property is set"); 672 | return ""; 673 | } 674 | 675 | } else { 676 | 677 | // Check the type of variant 678 | if (value.getValue().getValue().getClass().isArray()) { 679 | 680 | StringBuilder sb = new StringBuilder(); 681 | Object[] arr = (Object[]) value.getValue().getValue(); 682 | for (Object o : arr) { 683 | sb.append(o.toString()).append(";"); 684 | } 685 | sValue = sb.toString(); 686 | 687 | } else { 688 | sValue = value.getValue().getValue().toString(); 689 | } 690 | 691 | } 692 | 693 | StringBuilder valueLine = new StringBuilder(); 694 | 695 | valueLine.append(tagName).append(","); 696 | 697 | if (("ServerTimestamp").equals(returnTimestamp) || ("Both").equals(returnTimestamp)) { 698 | if (value.getServerTime() != null) valueLine.append(value.getServerTime().getJavaTime()); 699 | valueLine.append(","); 700 | } 701 | if (("SourceTimestamp").equals(returnTimestamp) || ("Both").equals(returnTimestamp)) { 702 | if (value.getSourceTime() != null) valueLine.append(value.getSourceTime().getJavaTime()); 703 | valueLine.append(","); 704 | } 705 | 706 | valueLine.append(sValue); 707 | valueLine.append(","); 708 | 709 | valueLine.append(value.getStatusCode().getValue()). 710 | append(System.getProperty("line.separator")); 711 | 712 | return valueLine.toString(); 713 | } 714 | 715 | // Special class as container to wrap subscription with the queue connected to a SubscribeOPCUANodes processor 716 | private static class SubscriptionConfig { 717 | 718 | private UaSubscription subscription; 719 | private BlockingQueue queue; 720 | 721 | SubscriptionConfig(UaSubscription subscription, BlockingQueue queue) { 722 | this.subscription = subscription; 723 | this.queue = queue; 724 | } 725 | 726 | UaSubscription getSubscription() { 727 | return subscription; 728 | } 729 | 730 | BlockingQueue getQueue() { 731 | return queue; 732 | } 733 | } 734 | 735 | // Custom SubscriptionListener to handle recreating subscription when transfer fails 736 | private class CustomSubscriptionListener implements UaSubscriptionManager.SubscriptionListener { 737 | 738 | @Override 739 | public void onPublishFailure(UaException exception) { 740 | getLogger().warn("Subscription publish failure: " + exception.getMessage() + ", status code: " + exception.getStatusCode()); 741 | } 742 | 743 | @Override 744 | public void onSubscriptionTransferFailed(UaSubscription subscription, StatusCode statusCode) { 745 | getLogger().warn("Subscription transfer failed: "+ statusCode + ". Trying to recreate subscription..."); 746 | 747 | // Get config from subscription object 748 | long minPublishInterval = (long) subscription.getRequestedPublishingInterval(); 749 | BlockingQueue queue = subscriptionMap.get(subscription.getSubscriptionId().toString()) 750 | .getQueue(); 751 | List readValueIds = new ArrayList<>(); 752 | DataChangeFilter df = null; 753 | for(UaMonitoredItem mi : subscription.getMonitoredItems()) { 754 | if(df == null) df = mi.getMonitoringFilter().decode(); 755 | readValueIds.add(mi.getReadValueId()); 756 | } 757 | 758 | // Try to clean up the previous subscription first 759 | unsubscribe(subscription.getSubscriptionId().toString()); 760 | 761 | // Recreate subscription with the previous MonitoredItems 762 | try { 763 | UaSubscription newSub = createSubscription(minPublishInterval); 764 | createMonitorItems(newSub, readValueIds, queue, df); 765 | putSubToMap(newSub, queue); 766 | } catch (Exception e) { 767 | e.printStackTrace(); 768 | getLogger().error("Recreating subscription failed!"); 769 | } 770 | } 771 | } 772 | 773 | } 774 | --------------------------------------------------------------------------------