├── 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 |
--------------------------------------------------------------------------------