diff --git a/WebContent/WEB-INF/applicationContext.xml b/WebContent/WEB-INF/applicationContext.xml
index b78b6d9959..0d87b1abc0 100644
--- a/WebContent/WEB-INF/applicationContext.xml
+++ b/WebContent/WEB-INF/applicationContext.xml
@@ -60,6 +60,8 @@
messages
+
+
diff --git a/WebContent/WEB-INF/dox/en/opcUaDS.htm b/WebContent/WEB-INF/dox/en/opcUaDS.htm
new file mode 100644
index 0000000000..3b13cb8354
--- /dev/null
+++ b/WebContent/WEB-INF/dox/en/opcUaDS.htm
@@ -0,0 +1,33 @@
+
+
+
@@ -107,6 +112,7 @@
+
@@ -215,6 +221,11 @@
+
+
+
+
+
diff --git a/WebContent/WEB-INF/jsp/dataSourceEdit.jsp b/WebContent/WEB-INF/jsp/dataSourceEdit.jsp
index 5b0055cdaa..e3c19e9906 100644
--- a/WebContent/WEB-INF/jsp/dataSourceEdit.jsp
+++ b/WebContent/WEB-INF/jsp/dataSourceEdit.jsp
@@ -519,5 +519,9 @@
test="${dataSource.type.id == applicationScope['constants.DataSourceVO.Types.RADIUINO']}">
+
+
+
\ No newline at end of file
diff --git a/WebContent/WEB-INF/jsp/dataSourceEdit/editOpcUa.jsp b/WebContent/WEB-INF/jsp/dataSourceEdit/editOpcUa.jsp
new file mode 100644
index 0000000000..4fe20a32f5
--- /dev/null
+++ b/WebContent/WEB-INF/jsp/dataSourceEdit/editOpcUa.jsp
@@ -0,0 +1,721 @@
+<%@ include file="/WEB-INF/jsp/include/tech.jsp"%>
+
+<%@page import="br.org.scadabr.KeyStoreType"%>
+<%@page import="br.org.scadabr.vo.dataSource.opcua.OpcUaDataType"%>
+<%@page import="br.org.scadabr.vo.dataSource.opcua.OpcUaIdentifierType"%>
+<%@page import="br.org.scadabr.vo.dataSource.opcua.OpcUaMessageSecurityType"%>
+<%@page import="br.org.scadabr.vo.dataSource.opcua.OpcUaSecurityPolicyType"%>
+
+
+
+
+
+
+
+<%@ include file="/WEB-INF/jsp/dataSourceEdit/dsHead.jspf"%>
+
+
+ " />
+
+
+
+
+
+
+
+ " />
+
+
+
+ " />
+
+
+
+
+
+
+
+
+
+ "/>" ${dataSource.messageSecurity.name() == 'NONE' ? 'selected' : ''} >
+ "/>" ${dataSource.messageSecurity.name() == 'SIGN' ? 'selected' : ''} >
+ "/>" ${dataSource.messageSecurity.name() == 'SIGN_ENCRYPT' ? 'selected' : ''} >
+
+
+
+
+
+
+
+ "/>" ${dataSource.securityPolicy.name() == 'NONE' ? 'selected' : ''} >
+ "/>" ${dataSource.securityPolicy.name() == 'BASIC_128_RSA_15' ? 'selected' : ''} >
+ "/>" ${dataSource.securityPolicy.name() == 'BASIC_256' ? 'selected' : ''} >
+ "/>" ${dataSource.securityPolicy.name() == 'BASIC_256_SHA_256' ? 'selected' : ''} >
+ "/>" ${dataSource.securityPolicy.name() == 'AES_128_SHA_256_RSA_OAEP' ? 'selected' : ''} >
+ "/>" ${dataSource.securityPolicy.name() == 'AES_256_SHA_256_RSA_PSS' ? 'selected' : ''} >
+
+
+
+
+
+ "" />
+
+
+
+
+
+ "/>" ${dataSource.keyStoreType.name() == 'JKS' ? 'selected' : ''} >
+ "/>" ${dataSource.keyStoreType.name() == 'PKCS11' ? 'selected' : ''} >
+ "/>" ${dataSource.keyStoreType.name() == 'DKS' ? 'selected' : ''} >
+ "/>" ${dataSource.keyStoreType.name() == 'JCEKS' ? 'selected' : ''} >
+ "/>" ${dataSource.keyStoreType.name() == 'PKCS12' ? 'selected' : ''} >
+
+
+
+
+
+ " />
+
+
+
+ " />
+
+
+
+
+
+ "/>" ${dataSource.trustStoreType.name() == 'JKS' ? 'selected' : ''} >
+ "/>" ${dataSource.trustStoreType.name() == 'PKCS11' ? 'selected' : ''} >
+ "/>" ${dataSource.trustStoreType.name() == 'DKS' ? 'selected' : ''} >
+ "/>" ${dataSource.trustStoreType.name() == 'JCEKS' ? 'selected' : ''} >
+ "/>" ${dataSource.trustStoreType.name() == 'PKCS12' ? 'selected' : ''} >
+
+
+
+
+
+ " />
+
+
+
+ " />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ "
+ onclick="searchServer();" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ "/>" >
+ "/>" >
+ "/>" >
+ "/>" >
+
+
+
+
+
+
+
+
+ "/>" >
+ "/>" >
+ "/>" >
+ "/>" >
+
+ "/>" >
+ "/>" >
+ "/>" >
+ "/>" >
+
+ "/>" >
+ "/>" >
+ "/>" >
+ "/>" >
+
+ "/>" >
+ "/>" >
+ "/>" >
+ "/>" >
+
+ "/>" >
+ "/>" >
+ "/>" >
+
+
+
+
+
+
+ " onclick="findTag();"/>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ "
+ onclick="btnAddTag();" />
+
+
+ <%@ include file="/WEB-INF/jsp/dataSourceEdit/dsFoot.jspf"%>
+
+
+
+
+
+
+
+
+
+
+ "/>" >
+ "/>" >
+ "/>" >
+ "/>" >
+
+ "/>" >
+ "/>" >
+ "/>" >
+ "/>" >
+
+ "/>" >
+ "/>" >
+ "/>" >
+ "/>" >
+
+ "/>" >
+ "/>" >
+ "/>" >
+ "/>" >
+
+ "/>" >
+ "/>" >
+ "/>" >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ "/>" >
+ "/>" >
+ "/>" >
+ "/>" >
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/WebContent/WEB-INF/lib/bcpkix-jdk18on-1.77.jar b/WebContent/WEB-INF/lib/bcpkix-jdk18on-1.77.jar
new file mode 100644
index 0000000000..e8b6021a2a
Binary files /dev/null and b/WebContent/WEB-INF/lib/bcpkix-jdk18on-1.77.jar differ
diff --git a/WebContent/WEB-INF/lib/bcprov-jdk18on-1.77.jar b/WebContent/WEB-INF/lib/bcprov-jdk18on-1.77.jar
new file mode 100644
index 0000000000..651d2fba5e
Binary files /dev/null and b/WebContent/WEB-INF/lib/bcprov-jdk18on-1.77.jar differ
diff --git a/WebContent/WEB-INF/lib/bcutil-jdk18on-1.77.jar b/WebContent/WEB-INF/lib/bcutil-jdk18on-1.77.jar
new file mode 100644
index 0000000000..4c154e27d6
Binary files /dev/null and b/WebContent/WEB-INF/lib/bcutil-jdk18on-1.77.jar differ
diff --git a/WebContent/WEB-INF/lib/bit-io-1.4.3.jar b/WebContent/WEB-INF/lib/bit-io-1.4.3.jar
new file mode 100644
index 0000000000..fca003226c
Binary files /dev/null and b/WebContent/WEB-INF/lib/bit-io-1.4.3.jar differ
diff --git a/WebContent/WEB-INF/lib/commons-codec-1.16.1.jar b/WebContent/WEB-INF/lib/commons-codec-1.16.1.jar
new file mode 100644
index 0000000000..f896649735
Binary files /dev/null and b/WebContent/WEB-INF/lib/commons-codec-1.16.1.jar differ
diff --git a/WebContent/WEB-INF/lib/commons-codec-1.9.jar b/WebContent/WEB-INF/lib/commons-codec-1.9.jar
deleted file mode 100644
index ef35f1c50d..0000000000
Binary files a/WebContent/WEB-INF/lib/commons-codec-1.9.jar and /dev/null differ
diff --git a/WebContent/WEB-INF/lib/commons-lang3-3.0.jar b/WebContent/WEB-INF/lib/commons-lang3-3.0.jar
deleted file mode 100644
index 780db952fe..0000000000
Binary files a/WebContent/WEB-INF/lib/commons-lang3-3.0.jar and /dev/null differ
diff --git a/WebContent/WEB-INF/lib/commons-lang3-3.14.0.jar b/WebContent/WEB-INF/lib/commons-lang3-3.14.0.jar
new file mode 100644
index 0000000000..da9302ff29
Binary files /dev/null and b/WebContent/WEB-INF/lib/commons-lang3-3.14.0.jar differ
diff --git a/WebContent/WEB-INF/lib/netty-buffer-4.1.106.Final.jar b/WebContent/WEB-INF/lib/netty-buffer-4.1.106.Final.jar
new file mode 100644
index 0000000000..e111e84e99
Binary files /dev/null and b/WebContent/WEB-INF/lib/netty-buffer-4.1.106.Final.jar differ
diff --git a/WebContent/WEB-INF/lib/netty-codec-4.1.106.Final.jar b/WebContent/WEB-INF/lib/netty-codec-4.1.106.Final.jar
new file mode 100644
index 0000000000..47516e7a40
Binary files /dev/null and b/WebContent/WEB-INF/lib/netty-codec-4.1.106.Final.jar differ
diff --git a/WebContent/WEB-INF/lib/netty-common-4.1.106.Final.jar b/WebContent/WEB-INF/lib/netty-common-4.1.106.Final.jar
new file mode 100644
index 0000000000..134188f66b
Binary files /dev/null and b/WebContent/WEB-INF/lib/netty-common-4.1.106.Final.jar differ
diff --git a/WebContent/WEB-INF/lib/netty-handler-4.1.106.Final.jar b/WebContent/WEB-INF/lib/netty-handler-4.1.106.Final.jar
new file mode 100644
index 0000000000..31560c197b
Binary files /dev/null and b/WebContent/WEB-INF/lib/netty-handler-4.1.106.Final.jar differ
diff --git a/WebContent/WEB-INF/lib/netty-resolver-4.1.106.Final.jar b/WebContent/WEB-INF/lib/netty-resolver-4.1.106.Final.jar
new file mode 100644
index 0000000000..4d5021a083
Binary files /dev/null and b/WebContent/WEB-INF/lib/netty-resolver-4.1.106.Final.jar differ
diff --git a/WebContent/WEB-INF/lib/netty-transport-4.1.106.Final.jar b/WebContent/WEB-INF/lib/netty-transport-4.1.106.Final.jar
new file mode 100644
index 0000000000..750d86bf4c
Binary files /dev/null and b/WebContent/WEB-INF/lib/netty-transport-4.1.106.Final.jar differ
diff --git a/WebContent/WEB-INF/lib/plc4j-api-0.12.0.jar b/WebContent/WEB-INF/lib/plc4j-api-0.12.0.jar
new file mode 100644
index 0000000000..a0704eccfa
Binary files /dev/null and b/WebContent/WEB-INF/lib/plc4j-api-0.12.0.jar differ
diff --git a/WebContent/WEB-INF/lib/plc4j-driver-opcua-0.12.0.jar b/WebContent/WEB-INF/lib/plc4j-driver-opcua-0.12.0.jar
new file mode 100644
index 0000000000..450b5633c3
Binary files /dev/null and b/WebContent/WEB-INF/lib/plc4j-driver-opcua-0.12.0.jar differ
diff --git a/WebContent/WEB-INF/lib/plc4j-spi-0.12.0.jar b/WebContent/WEB-INF/lib/plc4j-spi-0.12.0.jar
new file mode 100644
index 0000000000..41e704809d
Binary files /dev/null and b/WebContent/WEB-INF/lib/plc4j-spi-0.12.0.jar differ
diff --git a/WebContent/WEB-INF/lib/plc4j-transport-tcp-0.12.0.jar b/WebContent/WEB-INF/lib/plc4j-transport-tcp-0.12.0.jar
new file mode 100644
index 0000000000..ed0800cff3
Binary files /dev/null and b/WebContent/WEB-INF/lib/plc4j-transport-tcp-0.12.0.jar differ
diff --git a/WebContent/WEB-INF/lib/stax2-api-4.2.1.jar b/WebContent/WEB-INF/lib/stax2-api-4.2.1.jar
new file mode 100644
index 0000000000..28c6a08f40
Binary files /dev/null and b/WebContent/WEB-INF/lib/stax2-api-4.2.1.jar differ
diff --git a/WebContent/WEB-INF/lib/vavr-0.10.4.jar b/WebContent/WEB-INF/lib/vavr-0.10.4.jar
new file mode 100644
index 0000000000..5d1ab71ba6
Binary files /dev/null and b/WebContent/WEB-INF/lib/vavr-0.10.4.jar differ
diff --git a/WebContent/WEB-INF/lib/vavr-match-0.10.4.jar b/WebContent/WEB-INF/lib/vavr-match-0.10.4.jar
new file mode 100644
index 0000000000..a6f3028ca5
Binary files /dev/null and b/WebContent/WEB-INF/lib/vavr-match-0.10.4.jar differ
diff --git a/WebContent/WEB-INF/lib/woodstox-core-6.5.1.jar b/WebContent/WEB-INF/lib/woodstox-core-6.5.1.jar
new file mode 100644
index 0000000000..b22b384697
Binary files /dev/null and b/WebContent/WEB-INF/lib/woodstox-core-6.5.1.jar differ
diff --git a/src/br/org/scadabr/KeyStoreType.java b/src/br/org/scadabr/KeyStoreType.java
new file mode 100644
index 0000000000..0658795296
--- /dev/null
+++ b/src/br/org/scadabr/KeyStoreType.java
@@ -0,0 +1,22 @@
+package br.org.scadabr;
+
+public enum KeyStoreType {
+
+ JKS("jks"), PKCS11("pkcs11"), DKS("dks"), JCEKS("jceks"), PKCS12("pkcs12");
+
+ public static final KeyStoreType DEFAULT = KeyStoreType.PKCS12;
+
+ private final String name;
+
+ KeyStoreType(String name) {
+ this.name = name;
+ }
+
+ public String getCode() {
+ return this.name();
+ }
+
+ public String getDescription() {
+ return this.name;
+ }
+}
diff --git a/src/br/org/scadabr/OpcUaMaster.java b/src/br/org/scadabr/OpcUaMaster.java
new file mode 100644
index 0000000000..d26a7831c7
--- /dev/null
+++ b/src/br/org/scadabr/OpcUaMaster.java
@@ -0,0 +1,118 @@
+package br.org.scadabr;
+
+import br.org.scadabr.vo.dataSource.opcua.OpcUaDataSourceVO;
+import br.org.scadabr.vo.dataSource.opcua.OpcUaPointLocatorVO;
+import com.serotonin.mango.Common;
+import com.serotonin.mango.rt.dataImage.types.ImageValue;
+import com.serotonin.mango.rt.dataImage.types.MangoValue;
+import com.serotonin.mango.rt.maint.work.PlcConnectionClosingWorkItem;
+import com.serotonin.mango.rt.maint.work.WorkItem;
+import com.serotonin.mango.util.LoggingUtils;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.plc4x.java.api.PlcConnection;
+import org.apache.plc4x.java.api.PlcDriverManager;
+import org.apache.plc4x.java.api.messages.PlcReadResponse;
+import org.apache.plc4x.java.api.messages.PlcWriteResponse;
+import org.apache.plc4x.java.api.types.PlcResponseCode;
+import org.scada_lts.ds.opcua.TimeoutPlcDriverManager;
+
+import java.text.MessageFormat;
+import java.time.Duration;
+import java.time.temporal.ChronoUnit;
+import java.time.temporal.TemporalUnit;
+import java.util.ArrayList;
+
+public class OpcUaMaster {
+
+ private static final Log LOG = LogFactory.getLog(OpcUaMaster.class);
+
+ private final OpcUaDataSourceVO> dataSource;
+ private PlcConnection plcConnection;
+ private PlcDriverManager driverManager;
+
+ public OpcUaMaster(OpcUaDataSourceVO> dataSource) {
+ this.dataSource = dataSource;
+ this.driverManager = new TimeoutPlcDriverManager();
+ };
+
+ public void init() throws Exception {
+ terminate();
+ this.plcConnection = driverManager.getConnectionManager().getConnection(dataSource.getConnectionString());
+ }
+
+ public String getDataSourceXid() {
+ return dataSource.getXid();
+ }
+
+ public void terminate() throws Exception {
+ doClose(plcConnection);
+ }
+
+ public Object read(OpcUaPointLocatorVO pointLocator) throws Exception {
+ PlcConnection plcConnection = getConnetion();
+ PlcReadResponse readResponse = OpcUaUtils.sendRead(plcConnection, pointLocator.getTag(), pointLocator.getNodeId());
+ PlcResponseCode responseCode = readResponse.getResponseCode(pointLocator.getTag());
+ if (responseCode != PlcResponseCode.OK) {
+ throw new IllegalStateException(MessageFormat.format("Failed read nodeId: {0}, tag: {1}, value: {2}, code: {3}", pointLocator.getNodeId(), pointLocator.getTag(), readResponse.getObject(pointLocator.getTag()), responseCode.name()));
+ }
+ return readResponse.getObject(pointLocator.getTag());
+ }
+
+ public void write(OpcUaPointLocatorVO pointLocator, MangoValue value) throws Exception {
+ if(value == null) {
+ throw new IllegalArgumentException(MessageFormat.format("Failed write nodeId: {0}, tag: {1}, msg: {2}", pointLocator.getNodeId(), pointLocator.getTag(), "Value is null!"));
+ }
+ if(value instanceof ImageValue) {
+ throw new IllegalArgumentException(MessageFormat.format("Failed write nodeId: {0}, tag: {1}, msg: {2}", pointLocator.getNodeId(), pointLocator.getTag(), value.getClass().getName() + " is not supported!"));
+ }
+ Object valueToConvert = value.getObjectValue();
+ Object valueToSend = pointLocator.getDataType().convert(valueToConvert);
+ PlcConnection plcConnection = getConnetion();
+ PlcWriteResponse writeResponse = OpcUaUtils.sendWrite(plcConnection, pointLocator.getTag(), pointLocator.getNodeId(), valueToSend);
+ PlcResponseCode responseCode = writeResponse.getResponseCode(pointLocator.getTag());
+ if (responseCode != PlcResponseCode.OK)
+ throw new IllegalStateException(MessageFormat.format("Failed write nodeId: {0}, tag: {1}, valueToSend: {2}, code: {3}", pointLocator.getNodeId(), pointLocator.getTag(), valueToSend, responseCode.name()));
+ }
+
+ public int findNamespaceIndex(int numberOfAttempts, OpcUaPointLocatorVO pointLocator) {
+ try {
+ PlcConnection plcConnection = getConnetion();
+ return OpcUaUtils.findNamespaceIndex(plcConnection, numberOfAttempts, pointLocator);
+ } catch (Exception e) {
+ LOG.error(e.getMessage(), e);
+ return -1;
+ }
+ }
+
+ public boolean validateTag(OpcUaPointLocatorVO pointLocator, String tag) {
+ try {
+ PlcConnection plcConnection = getConnetion();
+ return OpcUaUtils.validateTag(plcConnection, pointLocator, tag);
+ } catch (Exception e) {
+ LOG.error(e.getMessage(), e);
+ return false;
+ }
+ }
+
+ public void ping() throws Exception {
+ PlcConnection plcConnection = getConnetion();
+ if(!plcConnection.isConnected()) {
+ throw new IllegalStateException("No connect!");
+ }
+ }
+
+ private static void doClose(PlcConnection plcConnection) {
+ if(plcConnection != null) {
+ WorkItem workItem = new PlcConnectionClosingWorkItem(plcConnection);
+ Common.ctx.getBackgroundProcessing().addWorkItem(workItem);
+ }
+ }
+
+ private PlcConnection getConnetion() throws Exception {
+ if(plcConnection == null) {
+ throw new IllegalStateException("No init!");
+ }
+ return plcConnection;
+ }
+}
diff --git a/src/br/org/scadabr/OpcUaUtils.java b/src/br/org/scadabr/OpcUaUtils.java
new file mode 100644
index 0000000000..42c0bc8a7c
--- /dev/null
+++ b/src/br/org/scadabr/OpcUaUtils.java
@@ -0,0 +1,104 @@
+package br.org.scadabr;
+
+import br.org.scadabr.vo.dataSource.opcua.OpcUaPointLocatorVO;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.plc4x.java.api.PlcConnection;
+import org.apache.plc4x.java.api.messages.*;
+import org.apache.plc4x.java.api.types.PlcResponseCode;
+
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.function.Consumer;
+
+public class OpcUaUtils {
+
+ private static final Log LOG = LogFactory.getLog(OpcUaUtils.class);
+
+ public OpcUaUtils() {
+ }
+
+ public static boolean validateTag(PlcConnection connection, OpcUaPointLocatorVO locator, String tag) throws ExecutionException, InterruptedException {
+ String nodeId = locator.getNodeId(); // Replace with the actual node ID
+
+ // Create a read request
+ PlcReadRequest.Builder builder = connection.readRequestBuilder();
+ builder.addTagAddress(tag, nodeId); // Add the node ID directly
+
+ PlcReadRequest readRequest = builder.build();
+ PlcReadResponse response = readRequest.execute().get();
+
+ return response.getResponseCode(tag) == PlcResponseCode.OK;
+ }
+
+ public static int findNamespaceIndex(PlcConnection connection, int numberOfAttempts, OpcUaPointLocatorVO opcUaPointLocatorVO) {
+ PlcReadResponse response;
+ String tag = opcUaPointLocatorVO.getTag();
+ OpcUaPointLocatorVO copied = opcUaPointLocatorVO.copy();
+ for(int i = 0; i < numberOfAttempts; i++) {
+ copied.setNamespaceIndex(i);
+ String nodeId = copied.getNodeId();
+ try {
+ response = sendRead(connection, tag, nodeId);
+ if(response != null && response.getResponseCode(tag) == PlcResponseCode.OK) {
+ Object value = read(response, tag);
+ LOG.info("tag: " + tag + ", nodeId: " + nodeId + ", value: " + value);
+ return i;
+ }
+ } catch (Exception e) {
+ LOG.error(e.getMessage(), e);
+ break;
+ }
+
+ }
+ return -1;
+ }
+
+ public static Object read(PlcReadResponse response, String varName) {
+ if (response.getResponseCode(varName) == PlcResponseCode.OK) {
+ return response.getObject(varName);
+ } else {
+ return null;
+ }
+ }
+
+ public static PlcReadResponse sendRead(PlcConnection connection, String tag, String nodeId) throws InterruptedException, ExecutionException {
+ PlcReadRequest.Builder builder = connection.readRequestBuilder();
+ builder.addTagAddress(tag, nodeId); // Add the node ID directly
+ PlcReadRequest readRequest = builder.build();
+ return readRequest.execute().get();
+ }
+
+ public static PlcWriteResponse sendWrite(PlcConnection connection, String tag, String nodeId, Object value) throws InterruptedException, ExecutionException {
+ PlcWriteRequest.Builder builder = connection.writeRequestBuilder();
+ builder.addTagAddress(tag, nodeId, value);
+ PlcWriteRequest readRequest = builder.build();
+ return readRequest.execute().get();
+ }
+
+ public static PlcSubscriptionResponse sendSubscribe(PlcConnection connection, String tag, String nodeId) throws InterruptedException, ExecutionException {
+ PlcSubscriptionRequest.Builder builder = connection.subscriptionRequestBuilder();
+ builder.addChangeOfStateTagAddress(tag, nodeId);
+ builder.addPreRegisteredConsumer(tag, new Consumer() {
+ @Override
+ public void accept(PlcSubscriptionEvent plcSubscriptionEvent) {
+ System.out.println("sub value");
+ Object value = plcSubscriptionEvent.getObject(tag);
+ System.out.println("sub value: " + value);
+ }
+ });
+ PlcSubscriptionRequest readRequest = builder.build();
+ return readRequest.execute().get();
+ }
+
+ public static PlcReadResponse sendPing(PlcConnection connection, String tag, String nodeId) throws InterruptedException, ExecutionException, TimeoutException {
+ return sendRead(connection, tag, nodeId);
+ }
+
+ private static PlcBrowseResponse sendBrowse(PlcConnection connection) throws InterruptedException, ExecutionException {
+ PlcBrowseRequest.Builder builder = connection.browseRequestBuilder();
+ PlcBrowseRequest readRequest = builder.build();
+ return readRequest.execute().get();
+ }
+}
diff --git a/src/br/org/scadabr/rt/dataSource/opcua/OpcUaDataSourceRT.java b/src/br/org/scadabr/rt/dataSource/opcua/OpcUaDataSourceRT.java
new file mode 100644
index 0000000000..511e4ec811
--- /dev/null
+++ b/src/br/org/scadabr/rt/dataSource/opcua/OpcUaDataSourceRT.java
@@ -0,0 +1,168 @@
+package br.org.scadabr.rt.dataSource.opcua;
+
+import br.org.scadabr.OpcUaMaster;
+import br.org.scadabr.vo.dataSource.opcua.OpcUaDataSourceVO;
+import br.org.scadabr.vo.dataSource.opcua.OpcUaPointLocatorVO;
+import com.serotonin.mango.DataTypes;
+import com.serotonin.mango.rt.dataImage.DataPointRT;
+import com.serotonin.mango.rt.dataImage.PointValueTime;
+import com.serotonin.mango.rt.dataImage.SetPointSource;
+import com.serotonin.mango.rt.dataImage.types.MangoValue;
+import com.serotonin.mango.rt.dataSource.PollingDataSource;
+import com.serotonin.mango.util.LoggingUtils;
+import com.serotonin.mango.vo.DataPointVO;
+import com.serotonin.web.i18n.LocalizableMessage;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.scada_lts.serorepl.utils.StringUtils;
+
+
+public class OpcUaDataSourceRT extends PollingDataSource {
+
+ private final Log LOG = LogFactory.getLog(OpcUaDataSourceRT.class);
+ public static final int POINT_READ_EXCEPTION_EVENT = 1;
+ public static final int DATA_SOURCE_EXCEPTION_EVENT = 2;
+ public static final int POINT_WRITE_EXCEPTION_EVENT = 3;
+ private OpcUaMaster opcMaster;
+ private final OpcUaDataSourceVO> vo;
+ private int timeoutCount = 0;
+ private int timeoutsToReconnect = 3;
+
+ public OpcUaDataSourceRT(OpcUaDataSourceVO> vo) {
+ super(vo);
+ this.vo = vo;
+ setPollingPeriod(vo.getUpdatePeriodType(), vo.getUpdatePeriods(),
+ vo.isQuantize());
+ }
+
+ @Override
+ protected void doPoll(long time) {
+
+ try {
+
+ if (timeoutCount >= timeoutsToReconnect) {
+ LOG.error("[OPC UA] Trying to reconnect ! :" + LoggingUtils.dataSourceInfo(this));
+ timeoutCount = 0;
+ initialize();
+ } else {
+ opcMaster.ping();
+ returnToNormal(DATA_SOURCE_EXCEPTION_EVENT, time);
+ }
+
+ } catch (Throwable e) {
+ LOG.error("[OPC UA] Poll Failed ! :" + LoggingUtils.info(e, this));
+ raiseEvent(
+ DATA_SOURCE_EXCEPTION_EVENT,
+ time,
+ true,
+ new LocalizableMessage("event.exception2", vo.getName(), e
+ .getMessage()));
+ timeoutCount++;
+ return;
+ }
+
+ for (DataPointRT dataPoint : getDataPoints()) {
+ OpcUaPointLocatorVO pointLocator = dataPoint.getVO().getPointLocator();
+ MangoValue mangoValue = null;
+ String value = "0";
+
+ try {
+ value = String.valueOf(opcMaster.read(pointLocator));
+ mangoValue = MangoValue.stringToValue(value, pointLocator.getDataTypeId());
+ dataPoint.updatePointValue(new PointValueTime(mangoValue, time));
+ returnToNormal(POINT_READ_EXCEPTION_EVENT, time, dataPoint);
+ } catch (Throwable e) {
+ LOG.error("[OPC UA] Poll Failed ! :" + LoggingUtils.info(e, this) + " - "
+ + LoggingUtils.dataPointInfo(dataPoint) + " - " + LoggingUtils.pointLocatorInfo(pointLocator));
+ raiseEvent(POINT_READ_EXCEPTION_EVENT, time, true,
+ new LocalizableMessage("event.exception2",
+ vo.getName(), e.getMessage()), dataPoint);
+ }
+ }
+ }
+
+ @Override
+ public void setPointValue(DataPointRT dataPoint, PointValueTime valueTime,
+ SetPointSource source) {
+ String tag = ((OpcUaPointLocatorVO) dataPoint.getVO().getPointLocator())
+ .getTag();
+ Object value = null;
+ if (dataPoint.getDataTypeId() == DataTypes.NUMERIC)
+ value = valueTime.getDoubleValue();
+ else if (dataPoint.getDataTypeId() == DataTypes.BINARY)
+ value = valueTime.getBooleanValue();
+ else if (dataPoint.getDataTypeId() == DataTypes.MULTISTATE)
+ value = valueTime.getIntegerValue();
+ else
+ value = valueTime.getStringValue();
+
+ try {
+ opcMaster.write(dataPoint.getVO().getPointLocator(), valueTime.getValue());
+ returnToNormal(POINT_WRITE_EXCEPTION_EVENT, System.currentTimeMillis(), dataPoint);
+ } catch (Throwable e) {
+ LOG.error(LoggingUtils.info(e, this, dataPoint), e);
+ raiseEvent(
+ POINT_WRITE_EXCEPTION_EVENT,
+ System.currentTimeMillis(),
+ true,
+ new LocalizableMessage("event.exception2", vo.getName(), e
+ .getMessage()), dataPoint);
+ }
+ }
+
+ public void initialize() {
+ if(opcMaster != null) {
+ try {
+ opcMaster.terminate();
+ } catch (Exception e) {
+ LOG.warn(LoggingUtils.info(e, this));
+ }
+ }
+
+ try {
+ this.opcMaster = new OpcUaMaster(vo);
+ opcMaster.init();
+ returnToNormal(DATA_SOURCE_EXCEPTION_EVENT, System.currentTimeMillis());
+ } catch (Throwable e) {
+ String message = e.getMessage();
+ if(e.getMessage() != null && e.getMessage().contains("Unknown Error")) {
+ message = "The OPC DA Server for the data source settings may not be found. ";
+ }
+ message = "Error while initializing data source: " + message;
+ LOG.error(message + e.getMessage(), e);
+ raiseEvent(
+ DATA_SOURCE_EXCEPTION_EVENT,
+ System.currentTimeMillis(),
+ true,
+ new LocalizableMessage("event.exception2", vo.getName(), message));
+ return;
+ }
+ super.initialize();
+ }
+
+ @Override
+ public void terminate() {
+ super.terminate();
+ try {
+ if(opcMaster != null)
+ opcMaster.terminate();
+ returnToNormal(DATA_SOURCE_EXCEPTION_EVENT, System.currentTimeMillis());
+ } catch (Throwable e) {
+ String message = e.getMessage();
+ if(e instanceof NullPointerException) {
+ message = "The client may not have been properly initialized. ";
+ }
+ message = "Error while terminating data source: " + message;
+ LOG.error(message + e.getMessage(), e);
+ raiseEvent(
+ DATA_SOURCE_EXCEPTION_EVENT,
+ System.currentTimeMillis(),
+ true,
+ new LocalizableMessage("event.exception2", vo.getName(), message));
+ }
+ }
+
+ public OpcUaDataSourceVO> getVo() {
+ return vo;
+ }
+}
diff --git a/src/br/org/scadabr/rt/dataSource/opcua/OpcUaPointLocatorRT.java b/src/br/org/scadabr/rt/dataSource/opcua/OpcUaPointLocatorRT.java
new file mode 100644
index 0000000000..c1c9cb6408
--- /dev/null
+++ b/src/br/org/scadabr/rt/dataSource/opcua/OpcUaPointLocatorRT.java
@@ -0,0 +1,23 @@
+package br.org.scadabr.rt.dataSource.opcua;
+
+import br.org.scadabr.vo.dataSource.opcua.OpcUaPointLocatorVO;
+import com.serotonin.mango.rt.dataSource.PointLocatorRT;
+
+public class OpcUaPointLocatorRT extends PointLocatorRT {
+ private final OpcUaPointLocatorVO vo;
+
+ public OpcUaPointLocatorRT(OpcUaPointLocatorVO vo) {
+ this.vo = vo;
+ }
+
+ @Override
+ public boolean isSettable() {
+ // TODO Auto-generated method stub
+ return vo.isSettable();
+ }
+
+ public OpcUaPointLocatorVO getVo() {
+ return vo;
+ }
+
+}
diff --git a/src/br/org/scadabr/vo/dataSource/opcua/OpcUaDataSourceVO.java b/src/br/org/scadabr/vo/dataSource/opcua/OpcUaDataSourceVO.java
new file mode 100644
index 0000000000..8522e96bc6
--- /dev/null
+++ b/src/br/org/scadabr/vo/dataSource/opcua/OpcUaDataSourceVO.java
@@ -0,0 +1,874 @@
+package br.org.scadabr.vo.dataSource.opcua;
+
+import br.org.scadabr.KeyStoreType;
+import br.org.scadabr.rt.dataSource.opc.OPCDataSource;
+import br.org.scadabr.rt.dataSource.opcua.OpcUaDataSourceRT;
+import com.serotonin.json.*;
+import com.serotonin.mango.Common;
+import com.serotonin.mango.rt.dataSource.DataSourceRT;
+import com.serotonin.mango.rt.event.type.AuditEventType;
+import com.serotonin.mango.util.ExportCodes;
+import com.serotonin.mango.vo.dataSource.DataSourceVO;
+import com.serotonin.mango.vo.dataSource.PointLocatorVO;
+import com.serotonin.mango.vo.event.EventTypeVO;
+import com.serotonin.util.SerializationHelper;
+import com.serotonin.util.StringUtils;
+import com.serotonin.web.dwr.DwrResponseI18n;
+import com.serotonin.web.i18n.LocalizableMessage;
+
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Paths;
+import java.text.MessageFormat;
+import java.util.List;
+import java.util.Map;
+
+@JsonRemoteEntity
+public class OpcUaDataSourceVO> extends
+ DataSourceVO {
+
+ public static final Type TYPE = Type.OPC_UA;
+
+ @Override
+ protected void addEventTypes(List eventTypes) {
+ eventTypes.add(createEventType(
+ OPCDataSource.POINT_READ_EXCEPTION_EVENT,
+ new LocalizableMessage("event.ds.pointRead")));
+ eventTypes.add(createEventType(
+ OPCDataSource.DATA_SOURCE_EXCEPTION_EVENT,
+ new LocalizableMessage("event.ds.dataSource")));
+ eventTypes.add(createEventType(
+ OPCDataSource.POINT_WRITE_EXCEPTION_EVENT,
+ new LocalizableMessage("event.ds.dataSource")));
+
+ }
+
+ private static final ExportCodes EVENT_CODES = new ExportCodes();
+ static {
+ EVENT_CODES.addElement(OPCDataSource.DATA_SOURCE_EXCEPTION_EVENT,
+ "DATA_SOURCE_EXCEPTION");
+ EVENT_CODES.addElement(OPCDataSource.POINT_READ_EXCEPTION_EVENT,
+ "POINT_READ_EXCEPTION");
+ EVENT_CODES.addElement(OPCDataSource.POINT_WRITE_EXCEPTION_EVENT,
+ "POINT_WRITE_EXCEPTION");
+
+ }
+
+ @Override
+ public DataSourceRT createDataSourceRT() {
+ return new OpcUaDataSourceRT(this);
+ }
+
+ @Override
+ public PointLocatorVO createPointLocator() {
+ return new OpcUaPointLocatorVO();
+ }
+
+ @Override
+ public LocalizableMessage getConnectionDescription() {
+ return new LocalizableMessage("common.default", this.serverName);
+ }
+
+ @Override
+ public ExportCodes getEventCodes() {
+ return EVENT_CODES;
+ }
+
+ @Override
+ public Type getType() {
+ return TYPE;
+ }
+
+ @JsonRemoteProperty
+ private int updatePeriodType = Common.TimePeriods.SECONDS;
+ @JsonRemoteProperty
+ private int updatePeriods = 1;
+ @JsonRemoteProperty
+ private boolean quantize;
+
+ @JsonRemoteProperty
+ private int serverPort = 4840;
+ @JsonRemoteProperty
+ private String serverHost = "localhost";
+ @JsonRemoteProperty
+ private String serverName = "";
+ @JsonRemoteProperty
+ private String serverPath = "";
+
+
+ /*
+ @ConfigurationParameter("discovery")
+ @BooleanDefaultValue(true)
+ @Description("Controls the feature of the discovery endpoint of an OPC UA server which every server\n" +
+ "will propagate over an '/discovery' endpoint. The most common issue here is that most servers are not correctly\n" +
+ "configured and propagate the wrong external IP or URL address. If that is the case you can disable the discovery by\n" +
+ "configuring it with a `false` value.\n" +
+ "\n" +
+ "The discovery phase is always conducted using `NONE` security policy.")
+
+ */
+ @JsonRemoteProperty
+ private boolean discovery = false;
+
+ /*
+ @ConfigurationParameter("username")
+ @Description("A username to authenticate to the OPCUA server with.")
+
+ */
+ @JsonRemoteProperty
+ private String user;
+
+ /*
+ @ConfigurationParameter("password")
+ @Description("A password to authenticate to the OPCUA server with.")
+
+ */
+ @JsonRemoteProperty
+ private String password;
+
+ /*
+ @ConfigurationParameter("security-policy")
+ @StringDefaultValue("NONE")
+ @Description("The security policy applied to communication channel between driver and OPC UA server.\n" +
+ "Default value assumes. Possible options are `NONE`, `Basic128Rsa15`, `Basic256`, `Basic256Sha256`, `Aes128_Sha256_RsaOaep`, `Aes256_Sha256_RsaPss`.")
+
+ */
+ private OpcUaSecurityPolicyType securityPolicy = OpcUaSecurityPolicyType.NONE;
+
+ /*
+ @ConfigurationParameter("message-security")
+ @StringDefaultValue("SIGN_ENCRYPT")
+ @Description("The security policy applied to messages exchanged after handshake phase.\n" +
+ "Possible options are `NONE`, `SIGN`, `SIGN_ENCRYPT`.\n" +
+ "This option is effective only when `securityPolicy` turns encryption (anything beyond `NONE`).")
+
+ */
+ private OpcUaMessageSecurityType messageSecurity = OpcUaMessageSecurityType.SIGN_ENCRYPT;
+
+ /*
+ @ConfigurationParameter("key-store-file")
+ @Description("The Keystore file used to lookup client certificate and its private key.")
+
+ */
+ @JsonRemoteProperty
+ private String keyStoreFile;
+
+ /*
+ @ConfigurationParameter("key-store-type")
+ @StringDefaultValue("pkcs12")
+ @Description("Keystore type used to access keystore and private key, defaults to PKCS (for Java 11+).\n" +
+ "Possible values are between others `jks`, `pkcs11`, `dks`, `jceks`.")
+
+ */
+ private KeyStoreType keyStoreType = KeyStoreType.PKCS12;
+
+ /*
+ @ConfigurationParameter("key-store-password")
+ @Description("Java keystore password used to access keystore and private key.")
+
+ */
+ @JsonRemoteProperty
+ private String keyStorePassword;
+
+ /*
+ @ConfigurationParameter("server-certificate-file")
+ @Description("Filesystem location where server certificate is located, supported formats are `DER` and `PEM`.")
+
+ */
+ @JsonRemoteProperty
+ private String serverCertificateFile;
+
+ /*
+ @ConfigurationParameter("trust-store-file")
+ @Description("The trust store file used to verify server certificates and its chain.")
+
+ */
+ @JsonRemoteProperty
+ private String trustStoreFile;
+
+ /*
+ @ConfigurationParameter("trust-store-type")
+ @StringDefaultValue("pkcs12")
+ @Description("Keystore type used to access keystore and private key, defaults to PKCS (for Java 11+).\n" +
+ "Possible values are between others `jks`, `pkcs11`, `dks`, `jceks`.")
+
+ */
+ private KeyStoreType trustStoreType = KeyStoreType.PKCS12;
+
+ /*
+ @ConfigurationParameter("trust-store-password")
+ @Description("Password used to open trust store.")
+
+ */
+ @JsonRemoteProperty
+ private String trustStorePassword;
+
+ /*
+ @ConfigurationParameter("channel-lifetime")
+ @LongDefaultValue(3600000)
+ @Description("Time for which negotiated secure channel, its keys and session remains open. Value in milliseconds, by default 60 minutes.")
+
+ */
+ @JsonRemoteProperty
+ private long channelLifetime = 3600000;
+
+ /*
+ @ConfigurationParameter("session-timeout")
+ @LongDefaultValue(120000)
+ @Description("Expiry time for opened secure session, value in milliseconds. Defaults to 2 minutes.")
+
+ */
+ @JsonRemoteProperty
+ private long sessionTimeout = 120000;
+
+ /*
+ @ConfigurationParameter("negotiation-timeout")
+ @LongDefaultValue(60000)
+ @Description("Timeout for all negotiation steps prior acceptance of application level operations - this timeout applies to open secure channel, create session and close calls. Defaults to 60 seconds.")
+
+ */
+ @JsonRemoteProperty
+ private long negotiationTimeout = 60000;
+
+ /*
+ @ConfigurationParameter("request-timeout")
+ @LongDefaultValue(30000)
+ @Description("Timeout for read/write/subscribe calls. Value in milliseconds.")
+
+ */
+ @JsonRemoteProperty
+ private long requestTimeout = 30000;
+
+ /*
+ @ConfigurationParameter("receive-buffer-size")
+ @IntDefaultValue(65535)
+ @Description("Maximum size of received TCP transport message chunk value in bytes.")
+
+ */
+ @JsonRemoteProperty
+ private int receiveBufferSize = 65535;
+
+ /*
+ @ConfigurationParameter("send-buffer-size")
+ @IntDefaultValue(65535)
+ @Description("Maximum size of sent transport message chunk.")
+
+ */
+ @JsonRemoteProperty
+ private int sendBufferSize = 65535;
+
+ /*
+ @ConfigurationParameter("max-message-size")
+ @IntDefaultValue(2097152)
+ @Description("Maximum size of complete message.")
+
+ */
+ @JsonRemoteProperty
+ private int maxMessageSize = 2097152;
+
+ /*
+ @ConfigurationParameter("max-chunk-count")
+ @IntDefaultValue(64)
+ @Description("Maximum number of chunks for both sent and received messages.")
+
+ */
+ @JsonRemoteProperty
+ private int maxChunkCount = 64;
+
+ /*
+ @ConfigurationParameter("keep-alive")
+ @BooleanDefaultValue(false)
+ @Description("Should keep-alive packets be sent?")
+
+ */
+ @JsonRemoteProperty
+ private boolean keepAlive = true;
+
+ /*
+ @ConfigurationParameter("no-delay")
+ @BooleanDefaultValue(true)
+ @Description("Should packets be sent instantly or should we give the OS some time to aggregate data.")
+
+ */
+ @JsonRemoteProperty
+ private boolean noDelay = true;
+
+ /*
+ @ConfigurationParameter("default-timeout")
+ @IntDefaultValue(1000)
+ @Description("Timeout after which a connection will be treated as disconnected.")
+
+ */
+ @JsonRemoteProperty
+ private long defaultTimeout = 1000;
+
+ @JsonRemoteProperty
+ private int creationMode;
+
+ public int getCreationMode() {
+ return creationMode;
+ }
+
+ public void setCreationMode(int creationMode) {
+ this.creationMode = creationMode;
+ }
+
+ public int getServerPort() {
+ return serverPort;
+ }
+
+ public void setServerPort(int serverPort) {
+ this.serverPort = serverPort;
+ }
+
+ public String getServerHost() {
+ return serverHost;
+ }
+
+ public void setServerHost(String serverHost) {
+ this.serverHost = serverHost;
+ }
+
+ public String getServerName() {
+ return serverName;
+ }
+
+ public void setServerName(String serverName) {
+ this.serverName = serverName;
+ }
+
+ public boolean isDiscovery() {
+ return discovery;
+ }
+
+ public void setDiscovery(boolean discovery) {
+ this.discovery = discovery;
+ }
+
+ public String getPassword() {
+ return password;
+ }
+
+ public void setPassword(String password) {
+ this.password = password;
+ }
+
+ public OpcUaSecurityPolicyType getSecurityPolicy() {
+ return securityPolicy;
+ }
+
+ public void setSecurityPolicy(OpcUaSecurityPolicyType securityPolicy) {
+ this.securityPolicy = securityPolicy;
+ }
+
+ public OpcUaMessageSecurityType getMessageSecurity() {
+ return messageSecurity;
+ }
+
+ public void setMessageSecurity(OpcUaMessageSecurityType messageSecurity) {
+ this.messageSecurity = messageSecurity;
+ }
+
+ public String getKeyStoreFile() {
+ return keyStoreFile;
+ }
+
+ public void setKeyStoreFile(String keyStoreFile) {
+ this.keyStoreFile = keyStoreFile;
+ }
+
+ public KeyStoreType getKeyStoreType() {
+ return keyStoreType;
+ }
+
+ public void setKeyStoreType(KeyStoreType keyStoreType) {
+ this.keyStoreType = keyStoreType;
+ }
+
+ public String getKeyStorePassword() {
+ return keyStorePassword;
+ }
+
+ public void setKeyStorePassword(String keyStorePassword) {
+ this.keyStorePassword = keyStorePassword;
+ }
+
+ public String getServerCertificateFile() {
+ return serverCertificateFile;
+ }
+
+ public void setServerCertificateFile(String serverCertificateFile) {
+ this.serverCertificateFile = serverCertificateFile;
+ }
+
+ public String getTrustStoreFile() {
+ return trustStoreFile;
+ }
+
+ public void setTrustStoreFile(String trustStoreFile) {
+ this.trustStoreFile = trustStoreFile;
+ }
+
+ public KeyStoreType getTrustStoreType() {
+ return trustStoreType;
+ }
+
+ public void setTrustStoreType(KeyStoreType trustStoreType) {
+ this.trustStoreType = trustStoreType;
+ }
+
+ public String getTrustStorePassword() {
+ return trustStorePassword;
+ }
+
+ public void setTrustStorePassword(String trustStorePassword) {
+ this.trustStorePassword = trustStorePassword;
+ }
+
+ public long getChannelLifetime() {
+ return channelLifetime;
+ }
+
+ public void setChannelLifetime(long channelLifetime) {
+ this.channelLifetime = channelLifetime;
+ }
+
+ public long getSessionTimeout() {
+ return sessionTimeout;
+ }
+
+ public void setSessionTimeout(long sessionTimeout) {
+ this.sessionTimeout = sessionTimeout;
+ }
+
+ public long getNegotiationTimeout() {
+ return negotiationTimeout;
+ }
+
+ public void setNegotiationTimeout(long negotiationTimeout) {
+ this.negotiationTimeout = negotiationTimeout;
+ }
+
+ public long getRequestTimeout() {
+ return requestTimeout;
+ }
+
+ public void setRequestTimeout(long requestTimeout) {
+ this.requestTimeout = requestTimeout;
+ }
+
+ public int getReceiveBufferSize() {
+ return receiveBufferSize;
+ }
+
+ public void setReceiveBufferSize(int receiveBufferSize) {
+ this.receiveBufferSize = receiveBufferSize;
+ }
+
+ public int getSendBufferSize() {
+ return sendBufferSize;
+ }
+
+ public void setSendBufferSize(int sendBufferSize) {
+ this.sendBufferSize = sendBufferSize;
+ }
+
+ public int getMaxMessageSize() {
+ return maxMessageSize;
+ }
+
+ public void setMaxMessageSize(int maxMessageSize) {
+ this.maxMessageSize = maxMessageSize;
+ }
+
+ public int getMaxChunkCount() {
+ return maxChunkCount;
+ }
+
+ public void setMaxChunkCount(int maxChunkCount) {
+ this.maxChunkCount = maxChunkCount;
+ }
+
+ public boolean isKeepAlive() {
+ return keepAlive;
+ }
+
+ public void setKeepAlive(boolean keepAlive) {
+ this.keepAlive = keepAlive;
+ }
+
+ public boolean isNoDelay() {
+ return noDelay;
+ }
+
+ public void setNoDelay(boolean noDelay) {
+ this.noDelay = noDelay;
+ }
+
+ public long getDefaultTimeout() {
+ return defaultTimeout;
+ }
+
+ public void setDefaultTimeout(long defaultTimeout) {
+ this.defaultTimeout = defaultTimeout;
+ }
+
+ public int getUpdatePeriodType() {
+ return updatePeriodType;
+ }
+
+ public void setUpdatePeriodType(int updatePeriodType) {
+ this.updatePeriodType = updatePeriodType;
+ }
+
+ public int getUpdatePeriods() {
+ return updatePeriods;
+ }
+
+ public void setUpdatePeriods(int updatePeriods) {
+ this.updatePeriods = updatePeriods;
+ }
+
+ public boolean isQuantize() {
+ return quantize;
+ }
+
+ public void setQuantize(boolean quantize) {
+ this.quantize = quantize;
+ }
+
+ public String getUser() {
+ return user;
+ }
+
+ public void setUser(String user) {
+ this.user = user;
+ }
+
+ public String getServerPath() {
+ return serverPath;
+ }
+
+ public void setServerPath(String serverPath) {
+ this.serverPath = serverPath;
+ }
+
+ public String getServerAddress() {
+ return MessageFormat.format("opcua:tcp://{0}:{1}{2}", serverHost, String.valueOf(serverPort), (StringUtils.isEmpty(serverPath) ? "" : "/" + serverPath));
+ }
+
+ public String getConnectionString() {
+
+ String address = getServerAddress();
+ String discoveryQuery = MessageFormat.format("?discovery={0}", isDiscovery());
+ String query = "";
+
+ /// Sec
+
+ if (getSecurityPolicy() != null) {
+ query += MessageFormat.format("&security-policy={0}", getSecurityPolicy().getDescription());
+
+ if(getSecurityPolicy() != OpcUaSecurityPolicyType.NONE) {
+
+ if (!StringUtils.isEmpty(getUser())) {
+ query += MessageFormat.format("&username={0}", getUser());
+ }
+
+ if (!StringUtils.isEmpty(getPassword())) {
+ query += MessageFormat.format("&password={0}", getPassword());
+ }
+
+ if (getMessageSecurity() != null) {
+ query += MessageFormat.format("&message-security={0}", getMessageSecurity().getCode());
+ }
+
+ if (!StringUtils.isEmpty(getServerCertificateFile())) {
+ String serverCertFilePath = URLEncoder.encode(
+ Paths.get(getServerCertificateFile()).toString(),
+ StandardCharsets.UTF_8
+ );
+ query += MessageFormat.format("&server-certificate-file={0}", serverCertFilePath);
+ }
+
+ if (!StringUtils.isEmpty(getKeyStoreFile())) {
+ String keyStoreFilePath = URLEncoder.encode(
+ Paths.get(getKeyStoreFile()).toString(),
+ StandardCharsets.UTF_8
+ );
+ query += MessageFormat.format("&key-store-file={0}" +
+ "&key-store-type={1}" +
+ "&key-store-password={2}", keyStoreFilePath, getKeyStoreType().getDescription(), getKeyStorePassword());
+ }
+
+ if (!StringUtils.isEmpty(getTrustStoreFile())) {
+ String trustStoreFilePath = URLEncoder.encode(
+ Paths.get(getTrustStoreFile()).toString(),
+ StandardCharsets.UTF_8
+ );
+ query += MessageFormat.format("&trust-store-file={0}" +
+ "&trust-store-type={1}" +
+ "&trust-store-password={2}", trustStoreFilePath, getTrustStoreType().getDescription(), getTrustStorePassword());
+ }
+ }
+ }
+ ///
+
+ if(getMaxChunkCount() > 0) {
+ query += MessageFormat.format("&encoding.max-chunk-count={0}", String.valueOf(getMaxChunkCount()));
+ }
+
+ if(getMaxMessageSize() > 0) {
+ query += MessageFormat.format("&encoding.max-message-size={0}", String.valueOf(getMaxMessageSize()));
+ }
+
+ if(getSendBufferSize() > 0) {
+ query += MessageFormat.format("&encoding.send-buffer-size={0}", String.valueOf(getSendBufferSize()));
+ }
+
+ if(getReceiveBufferSize() > 0) {
+ query += MessageFormat.format("&encoding.receive-buffer-size={0}", String.valueOf(getReceiveBufferSize()));
+ }
+
+ ///
+
+ if(getChannelLifetime() > 0) {
+ query += MessageFormat.format("&channel-lifetime={0}", String.valueOf(getChannelLifetime()));
+ }
+
+ if(getNegotiationTimeout() > 0) {
+ query += MessageFormat.format("&negotiation-timeout={0}", String.valueOf(getNegotiationTimeout()));
+ }
+
+ if(getRequestTimeout() > 0) {
+ query += MessageFormat.format("&request-timeout={0}", String.valueOf(getRequestTimeout()));
+ }
+
+ if(getSessionTimeout() > 0) {
+ query += MessageFormat.format("&session-timeout={0}", String.valueOf(getSessionTimeout()));
+ }
+
+ ///
+
+ query += MessageFormat.format("&tcp.keep-alive={0}"
+ + "&tcp.no-delay={1}", isKeepAlive(), isNoDelay());
+
+ if(getDefaultTimeout() > 0) {
+ query += MessageFormat.format("&tcp.default-timeout={0}", String.valueOf(getDefaultTimeout()));
+ }
+
+ return address + discoveryQuery + query;
+ }
+
+ @Override
+ public void validate(DwrResponseI18n response) {
+ super.validate(response);
+ if (StringUtils.isEmpty(serverHost))
+ response.addContextualMessage("host", "validate.required");
+ if (StringUtils.isEmpty(user))
+ response.addContextualMessage("user", "validate.required");
+ if (StringUtils.isEmpty(password))
+ response.addContextualMessage("password", "validate.required");
+ if (StringUtils.isEmpty(serverName))
+ response.addContextualMessage("server", "validate.required");
+ if (updatePeriods <= 0)
+ response.addContextualMessage("updatePeriods",
+ "validate.greaterThanZero");
+ }
+
+ @Override
+ protected void addPropertiesImpl(List list) {
+ AuditEventType.addPeriodMessage(list, "dsEdit.updatePeriod",
+ updatePeriodType, updatePeriods);
+ AuditEventType.addPropertyMessage(list, "dsEdit.opcua.host", serverHost);
+ AuditEventType.addPropertyMessage(list, "dsEdit.opcua.user", user);
+ AuditEventType
+ .addPropertyMessage(list, "dsEdit.opcua.password", password);
+ AuditEventType.addPropertyMessage(list, "dsEdit.opcua.server", serverName);
+
+ }
+
+ @Override
+ protected void addPropertyChangesImpl(List list, T from) {
+ AuditEventType.maybeAddPeriodChangeMessage(list,
+ "dsEdit.updatePeriod", from.getUpdatePeriodType(),
+ from.getUpdatePeriods(), updatePeriodType, updatePeriods);
+ AuditEventType.maybeAddPropertyChangeMessage(list, "dsEdit.opcua.host",
+ from.getServerHost(), serverHost);
+ AuditEventType.maybeAddPropertyChangeMessage(list, "dsEdit.opcua.user",
+ from.getUser(), user);
+ AuditEventType.maybeAddPropertyChangeMessage(list,
+ "dsEdit.opcua.password", from.getPassword(), password);
+ AuditEventType.maybeAddPropertyChangeMessage(list, "dsEdit.opcua.server",
+ from.getServerName(), serverName);
+
+ }
+
+ //
+ // /
+ // / Serialization
+ // /
+ //
+ private static final long serialVersionUID = -1;
+ private static final int version = 1;
+
+ private void writeObject(ObjectOutputStream out) throws IOException {
+ out.writeInt(version);
+
+ out.writeInt(updatePeriodType);
+ out.writeInt(updatePeriods);
+
+ SerializationHelper.writeSafeUTF(out, serverHost);
+ SerializationHelper.writeSafeUTF(out, user);
+ SerializationHelper.writeSafeUTF(out, password);
+ SerializationHelper.writeSafeUTF(out, serverName);
+
+ SerializationHelper.writeSafeUTF(out, keyStorePassword);
+ SerializationHelper.writeSafeUTF(out, trustStorePassword);
+
+ out.writeInt(maxChunkCount);
+ out.writeInt(serverPort);
+ out.writeInt(this.maxMessageSize);
+ out.writeInt(this.sendBufferSize);
+ out.writeInt(this.receiveBufferSize);
+
+ out.writeLong(this.defaultTimeout);
+ out.writeLong(this.channelLifetime);
+ out.writeLong(this.negotiationTimeout);
+ out.writeLong(this.sessionTimeout);
+ out.writeLong(this.requestTimeout);
+
+ out.writeBoolean(this.noDelay);
+ out.writeBoolean(this.keepAlive);
+ out.writeBoolean(this.discovery);
+
+ out.writeObject(this.keyStoreType);
+ out.writeObject(this.messageSecurity);
+ out.writeObject(this.securityPolicy);
+ out.writeObject(this.trustStoreType);
+
+ SerializationHelper.writeSafeUTF(out, keyStoreFile);
+ SerializationHelper.writeSafeUTF(out, trustStoreFile);
+ SerializationHelper.writeSafeUTF(out, serverCertificateFile);
+ SerializationHelper.writeSafeUTF(out, serverPath);
+ }
+
+ private void readObject(ObjectInputStream in) throws IOException,
+ ClassNotFoundException {
+ int ver = in.readInt();
+ if (ver == 1) {
+ updatePeriodType = in.readInt();
+ updatePeriods = in.readInt();
+
+ serverHost = SerializationHelper.readSafeUTF(in);
+ user = SerializationHelper.readSafeUTF(in);
+ password = SerializationHelper.readSafeUTF(in);
+ serverName = SerializationHelper.readSafeUTF(in);
+ keyStorePassword = SerializationHelper.readSafeUTF(in);
+ trustStorePassword = SerializationHelper.readSafeUTF(in);
+
+ maxChunkCount = in.readInt();
+ serverPort = in.readInt();
+ maxMessageSize = in.readInt();
+ sendBufferSize = in.readInt();
+ receiveBufferSize = in.readInt();
+
+ defaultTimeout = in.readLong();
+ channelLifetime = in.readLong();
+ negotiationTimeout = in.readLong();
+ sessionTimeout = in.readLong();
+ requestTimeout = in.readLong();
+
+ noDelay = in.readBoolean();
+ keepAlive = in.readBoolean();
+ discovery = in.readBoolean();
+
+ try {
+ keyStoreType = (KeyStoreType) in.readObject();
+ } catch (Exception e) {
+ keyStoreType = KeyStoreType.DEFAULT;
+ }
+
+ try {
+ messageSecurity = (OpcUaMessageSecurityType) in.readObject();
+ } catch (Exception e) {
+ messageSecurity = OpcUaMessageSecurityType.DEFAULT;
+ }
+
+ try {
+ securityPolicy = (OpcUaSecurityPolicyType) in.readObject();
+ } catch (Exception e) {
+ securityPolicy = OpcUaSecurityPolicyType.DEFAULT;
+ }
+
+ try {
+ trustStoreType = (KeyStoreType) in.readObject();
+ } catch (Exception e) {
+ trustStoreType = KeyStoreType.DEFAULT;
+ }
+
+ keyStoreFile = SerializationHelper.readSafeUTF(in);
+ trustStoreFile = SerializationHelper.readSafeUTF(in);
+ serverCertificateFile = SerializationHelper.readSafeUTF(in);
+ serverPath = SerializationHelper.readSafeUTF(in);
+ }
+ }
+
+ @Override
+ public void jsonDeserialize(JsonReader reader, JsonObject json)
+ throws JsonException {
+ super.jsonDeserialize(reader, json);
+ Integer value = deserializeUpdatePeriodType(json);
+ if (value != null)
+ updatePeriodType = value;
+
+ String keyStoreTypeJson = json.getString("keyStoreType");
+ if(keyStoreTypeJson != null) {
+ try {
+ keyStoreType = KeyStoreType.valueOf(keyStoreTypeJson);
+ } catch (Exception ex) {
+ keyStoreType = KeyStoreType.DEFAULT;
+ }
+ }
+
+ String messageSecurityJson = json.getString("messageSecurity");
+ if(messageSecurityJson != null) {
+ try {
+ messageSecurity = OpcUaMessageSecurityType.valueOf(messageSecurityJson);
+ } catch (Exception ex) {
+ messageSecurity = OpcUaMessageSecurityType.DEFAULT;
+ }
+ }
+
+ String securityPolicyJson = json.getString("securityPolicy");
+ if(securityPolicyJson != null) {
+ try {
+ securityPolicy = OpcUaSecurityPolicyType.valueOf(securityPolicyJson);
+ } catch (Exception ex) {
+ securityPolicy = OpcUaSecurityPolicyType.DEFAULT;
+ }
+ }
+
+ String trustStoreTypeJson = json.getString("trustStoreType");
+ if(trustStoreTypeJson != null) {
+ try {
+ trustStoreType = KeyStoreType.valueOf(trustStoreTypeJson);
+ } catch (Exception ex) {
+ trustStoreType = KeyStoreType.DEFAULT;
+ }
+ }
+ }
+
+ @Override
+ public void jsonSerialize(Map map) {
+ super.jsonSerialize(map);
+ serializeUpdatePeriodType(map, updatePeriodType);
+ map.put("keyStoreType", keyStoreType.name());
+ map.put("messageSecurity", messageSecurity.name());
+ map.put("securityPolicy", securityPolicy.name());
+ map.put("trustStoreType", trustStoreType.name());
+ }
+
+}
diff --git a/src/br/org/scadabr/vo/dataSource/opcua/OpcUaDataType.java b/src/br/org/scadabr/vo/dataSource/opcua/OpcUaDataType.java
new file mode 100644
index 0000000000..6a4b49de43
--- /dev/null
+++ b/src/br/org/scadabr/vo/dataSource/opcua/OpcUaDataType.java
@@ -0,0 +1,192 @@
+package br.org.scadabr.vo.dataSource.opcua;
+
+import com.serotonin.mango.DataTypes;
+
+import java.util.stream.Stream;
+
+public enum OpcUaDataType {
+
+ BOOL ("boolean", DataTypes.BINARY) {
+ public Object convert(Object value) throws Exception {
+ return OpcUaDataType.toBoolean(value);
+ }
+ },
+
+ SINT ("int 8", DataTypes.NUMERIC) {
+ public Object convert(Object value) throws Exception {
+ return toByte(value);
+ }
+ },
+
+ USINT ("uint 8", DataTypes.NUMERIC) {
+ public Object convert(Object value) throws Exception {
+ return toByte(value);
+ }
+ },
+
+ BYTE ("uint 8", DataTypes.NUMERIC) {
+ public Object convert(Object value) throws Exception {
+ return toByte(value);
+ }
+ },
+
+ INT ("int 16", DataTypes.NUMERIC) {
+ public Object convert(Object value) throws Exception {
+ return toShort(value);
+ }
+ },
+
+ UINT ("uint 16", DataTypes.NUMERIC) {
+ public Object convert(Object value) throws Exception {
+ return toShort(value);
+ }
+ },
+
+ WORD ("uint 16", DataTypes.NUMERIC) {
+ public Object convert(Object value) throws Exception {
+ return toShort(value);
+ }
+ },
+
+ DINT ("int 32", DataTypes.NUMERIC) {
+ public Object convert(Object value) throws Exception {
+ return toInt(value);
+ }
+ },
+
+ UDINT ("uint 32", DataTypes.NUMERIC) {
+ public Object convert(Object value) throws Exception {
+ return toInt(value);
+ }
+ },
+
+ DWORD ("uint 32", DataTypes.NUMERIC) {
+ public Object convert(Object value) throws Exception {
+ return toInt(value);
+ }
+ },
+
+ LINT ("int 64", DataTypes.NUMERIC) {
+ public Object convert(Object value) throws Exception {
+ return toLong(value);
+ }
+ },
+
+ ULINT ("uint 64", DataTypes.NUMERIC) {
+ public Object convert(Object value) throws Exception {
+ return toLong(value);
+ }
+ },
+
+ LWORD ("uint 64", DataTypes.NUMERIC) {
+ public Object convert(Object value) throws Exception {
+ return toLong(value);
+ }
+ },
+
+ REAL ("float", DataTypes.NUMERIC) {
+ public Object convert(Object value) throws Exception {
+ return toFloat(value);
+ }
+ },
+
+ LREAL ("double", DataTypes.NUMERIC) {
+ public Object convert(Object value) throws Exception {
+ return toDouble(value);
+ }
+ },
+
+ CHAR ("char", DataTypes.ALPHANUMERIC) {
+ public Object convert(Object value) throws Exception {
+ return toCharOrString(value);
+ }
+ },
+
+ WCHAR ("2 byte char", DataTypes.ALPHANUMERIC) {
+ public Object convert(Object value) throws Exception {
+ return toCharOrString(value);
+ }
+ },
+
+ STRING ("utf-8", DataTypes.ALPHANUMERIC) {
+ public Object convert(Object value) throws Exception {
+ return OpcUaDataType.toString(value);
+ }
+ },
+
+ WSTRING ("utf-16", DataTypes.ALPHANUMERIC) {
+ public Object convert(Object value) throws Exception {
+ return OpcUaDataType.toString(value);
+ }
+ };
+
+ public static final OpcUaDataType DEFAULT = OpcUaDataType.STRING;
+
+ private final String decription;
+ private final int dataTypesId;
+
+ OpcUaDataType(String decription, int dataTypesId) {
+ this.decription = decription;
+ this.dataTypesId = dataTypesId;
+ }
+
+ public String getCode() {
+ return this.name();
+ }
+
+ public int getDataTypesId() {
+ return dataTypesId;
+ }
+
+ public String getDescription() {
+ return getCode() + " (" + this.decription + ")";
+ }
+
+ public abstract Object convert(Object value) throws Exception;
+
+ public static OpcUaDataType valueOf(int id) {
+ return Stream.of(OpcUaDataType.values()).filter(a -> a.getDataTypesId() == id).findAny().orElse(OpcUaDataType.STRING);
+ }
+
+ private static boolean toBoolean(Object value) {
+ return Boolean.parseBoolean(String.valueOf(value));
+ }
+
+ private static Object toCharOrString(Object value) {
+ String result = String.valueOf(value);
+ char[] resultRaw = result.toCharArray();
+ return resultRaw.length == 1 ? resultRaw[0] : result;
+ }
+
+ private static byte toByte(Object value) {
+ return Byte.parseByte(toStringForInt(value));
+ }
+
+ private static short toShort(Object value) {
+ return Short.parseShort(toStringForInt(value));
+ }
+
+ private static int toInt(Object value) {
+ return Integer.parseInt(toStringForInt(value));
+ }
+
+ private static long toLong(Object value) {
+ return Long.parseLong(toStringForInt(value));
+ }
+
+ private static double toDouble(Object value) {
+ return Double.parseDouble(String.valueOf(value));
+ }
+
+ private static float toFloat(Object value) {
+ return Float.parseFloat(String.valueOf(value));
+ }
+
+ private static String toStringForInt(Object value) {
+ return String.valueOf(value).replace(".0", "");
+ }
+
+ private static String toString(Object value) {
+ return String.valueOf(value);
+ }
+}
diff --git a/src/br/org/scadabr/vo/dataSource/opcua/OpcUaIdentifierType.java b/src/br/org/scadabr/vo/dataSource/opcua/OpcUaIdentifierType.java
new file mode 100644
index 0000000000..ddcdeb1ab2
--- /dev/null
+++ b/src/br/org/scadabr/vo/dataSource/opcua/OpcUaIdentifierType.java
@@ -0,0 +1,22 @@
+package br.org.scadabr.vo.dataSource.opcua;
+
+public enum OpcUaIdentifierType {
+
+ STRING("s"), NUMERIC("i"), BINARY("b"), GUID("g");
+
+ public static final OpcUaIdentifierType DEFAULT = OpcUaIdentifierType.STRING;
+
+ private final String code;
+
+ OpcUaIdentifierType(String code) {
+ this.code = code;
+ }
+
+ public String getCode() {
+ return code;
+ }
+
+ public String getDescription() {
+ return this.name();
+ }
+}
diff --git a/src/br/org/scadabr/vo/dataSource/opcua/OpcUaItem.java b/src/br/org/scadabr/vo/dataSource/opcua/OpcUaItem.java
new file mode 100644
index 0000000000..768ff0245d
--- /dev/null
+++ b/src/br/org/scadabr/vo/dataSource/opcua/OpcUaItem.java
@@ -0,0 +1,87 @@
+package br.org.scadabr.vo.dataSource.opcua;
+
+public class OpcUaItem {
+ private String identifier;
+ private OpcUaIdentifierType identifierType;
+ private String tag;
+ private OpcUaDataType dataType;
+ private int dataTypeId;
+ private boolean settable;
+ private boolean validate;
+ private int namespaceIndex;
+
+ public OpcUaItem(boolean validate, OpcUaPointLocatorVO pointLocator) {
+ this.validate = validate;
+ this.tag = pointLocator.getTag();
+ this.settable = pointLocator.isSettable();
+ this.identifier = pointLocator.getIdentifier();
+ this.identifierType = pointLocator.getIdentifierType();
+ this.dataType = pointLocator.getDataType();
+ this.dataTypeId = pointLocator.getDataType().getDataTypesId();
+ this.namespaceIndex = pointLocator.getNamespaceIndex();
+ }
+
+ public boolean isValidate() {
+ return this.validate;
+ }
+
+ public void setValidate(boolean validate) {
+ this.validate = validate;
+ }
+
+ public String getTag() {
+ return this.tag;
+ }
+
+ public void setTag(String tag) {
+ this.tag = tag;
+ }
+
+ public OpcUaDataType getDataType() {
+ return this.dataType;
+ }
+
+ public void setDataType(OpcUaDataType dataType) {
+ this.dataType = dataType;
+ }
+
+ public boolean isSettable() {
+ return this.settable;
+ }
+
+ public void setSettable(boolean settable) {
+ this.settable = settable;
+ }
+
+ public String getIdentifier() {
+ return identifier;
+ }
+
+ public void setIdentifier(String identifier) {
+ this.identifier = identifier;
+ }
+
+ public OpcUaIdentifierType getIdentifierType() {
+ return identifierType;
+ }
+
+ public void setIdentifierType(OpcUaIdentifierType identifierType) {
+ this.identifierType = identifierType;
+ }
+
+ public int getDataTypeId() {
+ return dataTypeId;
+ }
+
+ public void setDataTypeId(int dataTypeId) {
+ this.dataTypeId = dataTypeId;
+ }
+
+ public int getNamespaceIndex() {
+ return namespaceIndex;
+ }
+
+ public void setNamespaceIndex(int namespaceIndex) {
+ this.namespaceIndex = namespaceIndex;
+ }
+}
diff --git a/src/br/org/scadabr/vo/dataSource/opcua/OpcUaMessageSecurityType.java b/src/br/org/scadabr/vo/dataSource/opcua/OpcUaMessageSecurityType.java
new file mode 100644
index 0000000000..55b75c5fc3
--- /dev/null
+++ b/src/br/org/scadabr/vo/dataSource/opcua/OpcUaMessageSecurityType.java
@@ -0,0 +1,22 @@
+package br.org.scadabr.vo.dataSource.opcua;
+
+public enum OpcUaMessageSecurityType {
+ NONE("None"), SIGN("Sign"), SIGN_ENCRYPT("SignAndEncrypt");
+
+ public static final OpcUaMessageSecurityType DEFAULT = OpcUaMessageSecurityType.SIGN_ENCRYPT;
+
+ private final String decription;
+
+ OpcUaMessageSecurityType(String decription) {
+ this.decription = decription;
+ }
+
+ public String getCode() {
+ return this.name();
+ }
+
+ public String getDescription() {
+ return this.decription;
+ }
+
+}
diff --git a/src/br/org/scadabr/vo/dataSource/opcua/OpcUaPointLocatorVO.java b/src/br/org/scadabr/vo/dataSource/opcua/OpcUaPointLocatorVO.java
new file mode 100644
index 0000000000..dd6ca53468
--- /dev/null
+++ b/src/br/org/scadabr/vo/dataSource/opcua/OpcUaPointLocatorVO.java
@@ -0,0 +1,237 @@
+package br.org.scadabr.vo.dataSource.opcua;
+
+import br.org.scadabr.rt.dataSource.opcua.OpcUaPointLocatorRT;
+import com.serotonin.json.*;
+import com.serotonin.mango.rt.dataSource.PointLocatorRT;
+import com.serotonin.mango.rt.event.type.AuditEventType;
+import com.serotonin.mango.vo.dataSource.AbstractPointLocatorVO;
+import com.serotonin.util.SerializationHelper;
+import com.serotonin.util.StringUtils;
+import com.serotonin.web.dwr.DwrResponseI18n;
+import com.serotonin.web.i18n.LocalizableMessage;
+
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.text.MessageFormat;
+import java.util.List;
+import java.util.Map;
+
+public class OpcUaPointLocatorVO extends AbstractPointLocatorVO implements
+ JsonSerializable {
+
+ @Override
+ public PointLocatorRT createRuntime() {
+ return new OpcUaPointLocatorRT(this);
+ }
+
+ @Override
+ public LocalizableMessage getConfigurationDescription() {
+ return new LocalizableMessage("common.tp.description", new LocalizableMessage("dsEdit.opc.tag"), this.tag);
+ }
+
+ @Override
+ public int getDataTypeId() {
+ return dataType.getDataTypesId();
+ }
+
+ public void setDataType(OpcUaDataType dataType) {
+ this.dataType = dataType;
+ }
+
+ @Override
+ public boolean isSettable() {
+ return settable;
+ }
+
+ public void setSettable(boolean settable) {
+ this.settable = settable;
+ }
+
+ @JsonRemoteProperty
+ private String tag = "";
+ private OpcUaDataType dataType = OpcUaDataType.DEFAULT;
+ @JsonRemoteProperty
+ private boolean settable;
+ private OpcUaIdentifierType identifierType = OpcUaIdentifierType.DEFAULT;
+ @JsonRemoteProperty
+ private String identifier;
+ @JsonRemoteProperty
+ private int namespaceIndex;
+ @JsonRemoteProperty
+ private String attributes;
+
+ public OpcUaPointLocatorVO() {
+ }
+
+ private OpcUaPointLocatorVO(OpcUaPointLocatorVO pointLocator) {
+ this.tag = pointLocator.getTag();
+ this.dataType = pointLocator.getDataType();
+ this.settable = pointLocator.isSettable();
+ this.identifierType = pointLocator.getIdentifierType();
+ this.identifier = pointLocator.getIdentifier();
+ this.namespaceIndex = pointLocator.getNamespaceIndex();
+ this.attributes = pointLocator.getAttributes();
+ }
+
+ @Override
+ public void validate(DwrResponseI18n response) {
+
+ }
+
+ @Override
+ public void addProperties(List list) {
+ AuditEventType.addDataTypeMessage(list, "dsEdit.opcua.dataType", dataType);
+ AuditEventType.addPropertyMessage(list, "dsEdit.opcua.tag", tag);
+ AuditEventType.addPropertyMessage(list, "dsEdit.settable", settable);
+ AuditEventType.addPropertyMessage(list, "dsEdit.opcua.identifier", identifier);
+ AuditEventType.addPropertyMessage(list, "dsEdit.opcua.identifierType", identifierType);
+ AuditEventType.addPropertyMessage(list, "dsEdit.opcua.namespaceIndex", namespaceIndex);
+ AuditEventType.addPropertyMessage(list, "dsEdit.opcua.attributes", attributes);
+ }
+
+ @Override
+ public void addPropertyChanges(List list, Object o) {
+ OpcUaPointLocatorVO from = (OpcUaPointLocatorVO) o;
+
+ AuditEventType.maybeAddObjectChangeMessage(list,
+ "dsEdit.opcua.dataType", from.dataType, dataType);
+ AuditEventType.maybeAddPropertyChangeMessage(list, "dsEdit.opcua.tag",
+ from.tag, tag);
+ AuditEventType.maybeAddPropertyChangeMessage(list, "dsEdit.settable",
+ from.settable, settable);
+
+ AuditEventType.maybeAddPropertyChangeMessage(list, "dsEdit.opcua.identifier",
+ from.identifier, identifier);
+
+ AuditEventType.maybeAddPropertyChangeMessage(list, "dsEdit.opcua.identifierType",
+ from.identifierType, identifierType);
+
+ AuditEventType.maybeAddPropertyChangeMessage(list, "dsEdit.opcua.namespaceIndex",
+ from.namespaceIndex, namespaceIndex);
+
+ AuditEventType.maybeAddPropertyChangeMessage(list, "dsEdit.opcua.attributes",
+ from.attributes, attributes);
+ }
+
+ private static final long serialVersionUID = -1;
+ private static final int version = 1;
+
+ private void writeObject(ObjectOutputStream out) throws IOException {
+ out.writeInt(version);
+ SerializationHelper.writeSafeUTF(out, tag);
+ out.writeObject(dataType);
+ out.writeBoolean(settable);
+ SerializationHelper.writeSafeUTF(out, identifier);
+ out.writeObject(identifierType);
+ out.writeInt(namespaceIndex);
+ SerializationHelper.writeSafeUTF(out, attributes);
+ }
+
+ private void readObject(ObjectInputStream in) throws IOException,
+ ClassNotFoundException {
+ int ver = in.readInt();
+ if (ver == 1) {
+ tag = SerializationHelper.readSafeUTF(in);
+ try {
+ dataType = (OpcUaDataType) in.readObject();
+ } catch (Exception e) {
+ dataType = OpcUaDataType.DEFAULT;
+ }
+ settable = in.readBoolean();
+ identifier = SerializationHelper.readSafeUTF(in);
+ try {
+ identifierType = (OpcUaIdentifierType) in.readObject();
+ } catch (Exception e) {
+ identifierType = OpcUaIdentifierType.DEFAULT;
+ }
+ namespaceIndex = in.readInt();
+ attributes = SerializationHelper.readSafeUTF(in);
+ }
+ }
+
+ @Override
+ public void jsonDeserialize(JsonReader reader, JsonObject json)
+ throws JsonException {
+
+ String dataTypeJson = json.getString("dataType");
+ if(dataTypeJson != null) {
+ try {
+ dataType = OpcUaDataType.valueOf(dataTypeJson);
+ } catch (Exception ex) {
+ dataType = OpcUaDataType.DEFAULT;
+ }
+ }
+
+ String identifierTypeJson = json.getString("identifierType");
+ if(identifierTypeJson != null) {
+ try {
+ identifierType = OpcUaIdentifierType.valueOf(identifierTypeJson);
+ } catch (Exception ex) {
+ identifierType = OpcUaIdentifierType.DEFAULT;
+ }
+ }
+ }
+
+ @Override
+ public void jsonSerialize(Map map) {
+ serializeDataType(map);
+ map.put("identifierType", identifierType.name());
+ map.put("dataType", dataType.name());
+ }
+
+ public String getTag() {
+ return tag;
+ }
+
+ public void setTag(String tag) {
+ this.tag = tag;
+ }
+
+ public OpcUaDataType getDataType() {
+ return dataType;
+ }
+
+ public OpcUaIdentifierType getIdentifierType() {
+ return identifierType;
+ }
+
+ public void setIdentifierType(OpcUaIdentifierType identifierType) {
+ this.identifierType = identifierType;
+ }
+
+ public String getIdentifier() {
+ return identifier;
+ }
+
+ public void setIdentifier(String identifier) {
+ this.identifier = identifier;
+ }
+
+ public int getNamespaceIndex() {
+ return namespaceIndex;
+ }
+
+ public void setNamespaceIndex(int namespaceIndex) {
+ this.namespaceIndex = namespaceIndex;
+ }
+
+ public String getAttributes() {
+ return attributes;
+ }
+
+ public void setAttributes(String attributes) {
+ this.attributes = attributes;
+ }
+
+ public String getNodeId() {
+ if (!StringUtils.isEmpty(getAttributes())) {
+ return MessageFormat.format("ns={0};{1}={2};{3};{4}", getNamespaceIndex(), getIdentifierType().getCode(), getIdentifier(), getAttributes(), getDataType());
+ }
+ return MessageFormat.format("ns={0};{1}={2};{3}", getNamespaceIndex(), getIdentifierType().getCode(), getIdentifier(), getDataType());
+ }
+
+ public OpcUaPointLocatorVO copy() {
+ return new OpcUaPointLocatorVO(this);
+ }
+}
diff --git a/src/br/org/scadabr/vo/dataSource/opcua/OpcUaSecurityPolicyType.java b/src/br/org/scadabr/vo/dataSource/opcua/OpcUaSecurityPolicyType.java
new file mode 100644
index 0000000000..3459e024bc
--- /dev/null
+++ b/src/br/org/scadabr/vo/dataSource/opcua/OpcUaSecurityPolicyType.java
@@ -0,0 +1,27 @@
+package br.org.scadabr.vo.dataSource.opcua;
+
+public enum OpcUaSecurityPolicyType {
+
+ NONE("None"),
+ BASIC_128_RSA_15("Basic128Rsa15"),
+ BASIC_256("Basic256"),
+ BASIC_256_SHA_256("Basic256Sha256"),
+ AES_128_SHA_256_RSA_OAEP("Aes128_Sha256_RsaOaep"),
+ AES_256_SHA_256_RSA_PSS("Aes256_Sha256_RsaPss");
+
+ public static final OpcUaSecurityPolicyType DEFAULT = OpcUaSecurityPolicyType.NONE;
+
+ private final String description;
+
+ OpcUaSecurityPolicyType(String description) {
+ this.description = description;
+ }
+
+ public String getCode() {
+ return this.name();
+ }
+
+ public String getDescription() {
+ return description;
+ }
+}
diff --git a/src/com/serotonin/mango/Common.java b/src/com/serotonin/mango/Common.java
index f07c3b0dd5..75eb04daaf 100644
--- a/src/com/serotonin/mango/Common.java
+++ b/src/com/serotonin/mango/Common.java
@@ -29,6 +29,7 @@
import javax.servlet.http.HttpServletRequest;
+import com.serotonin.mango.vo.*;
import com.serotonin.mango.web.mvc.controller.ScadaLocaleUtils;
import org.scada_lts.monitor.IMonitoredValues;
import gnu.io.CommPortIdentifier;
@@ -53,8 +54,6 @@
import com.serotonin.mango.util.ExportCodes;
import com.serotonin.mango.view.View;
import com.serotonin.mango.view.custom.CustomView;
-import com.serotonin.mango.vo.CommPortProxy;
-import com.serotonin.mango.vo.User;
import com.serotonin.mango.web.ContextWrapper;
import org.scada_lts.monitor.ConcurrentMonitoredValues;
import com.serotonin.timer.CronTimerTrigger;
@@ -219,6 +218,13 @@ public static LocalizableMessage getPeriodDescription(int periodType,
new LocalizableMessage(periodKey));
}
+ public static LocalizableMessage getPeriodDescription(TimePeriod periodType,
+ int periods) {
+ String periodKey = periodType.getKey();
+ return new LocalizableMessage("common.tp.description", periods,
+ new LocalizableMessage(periodKey));
+ }
+
//
// Session user
public static User getUser() {
diff --git a/src/com/serotonin/mango/MangoContextListener.java b/src/com/serotonin/mango/MangoContextListener.java
index 74a5f8f884..a9efad81d9 100644
--- a/src/com/serotonin/mango/MangoContextListener.java
+++ b/src/com/serotonin/mango/MangoContextListener.java
@@ -73,6 +73,7 @@
import org.scada_lts.scripting.SandboxContextFactory;
import org.scada_lts.service.HighestAlarmLevelServiceWithCache;
import org.scada_lts.service.IHighestAlarmLevelService;
+import org.scada_lts.utils.ThreadInfoApiUtils;
import org.scada_lts.web.beans.ApplicationBeans;
import javax.servlet.ServletContext;
@@ -86,6 +87,7 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.stream.Collectors;
import static com.serotonin.mango.util.ThreadPoolExecutorUtils.createPool;
import static org.scada_lts.utils.UploadFileUtils.loadGraphics;
@@ -227,12 +229,47 @@ public void contextDestroyed(ServletContextEvent evt) {
utilitiesTerminate(ctx);
highPriorityServiceTerminate();
databaseTerminate(ctx);
+ otherThreadsTerminate();
Common.ctx = null;
log.info("Scada-LTS context terminated");
}
+ private void otherThreadsTerminate() {
+
+ List threads = ThreadInfoApiUtils.getThreads()
+ .stream()
+ .filter(a -> toInterrupt(a))
+ .collect(Collectors.toList());
+ log.info("Threads size before terminating: " + threads.size());
+
+ List notInterruptedthreads = threads.stream()
+ .peek(a -> {
+ log.info(a.getName() + " - terminating");
+ try {
+ a.interrupt();
+ a.stop();
+ } catch (Throwable ex) {
+ log.info(a.getName() + " - terminating - error: " + ex.getMessage());
+ }
+ log.info(a.getName() + " - terminated");
+ })
+ .filter(a -> !a.isInterrupted())
+ .collect(Collectors.toList());
+
+ log.info("Threads size after terminating: " + notInterruptedthreads.size());
+ }
+
+ private static boolean toInterrupt(Thread a) {
+ return !a.isDaemon() && !a.getName().equals("main") && !a.getName().equals(Thread.currentThread().getName())
+ && !a.getName().startsWith("MessageBroker")
+ && !a.getName().startsWith("clientOutboundChannel")
+ && !a.getName().startsWith("clientInboundChannel")
+ && !a.getName().startsWith("Catalina-utility")
+ && !a.getName().startsWith("DefaultQuartzScheduler_");
+ }
+
private void highPriorityServiceTerminate() {
Common.timer.cancel();
}
@@ -345,6 +382,9 @@ private void constantsInitialize(ServletContext ctx) {
DataSourceVO.Type.JMX.getId());
ctx.setAttribute("constants.DataSourceVO.Types.MQTT",
DataSourceVO.Type.MQTT.getId());
+ ctx.setAttribute("constants.DataSourceVO.Types.OPC_UA",
+ DataSourceVO.Type.OPC_UA.getId());
+
ctx.setAttribute("constants.Permissions.DataPointAccessTypes.NONE",
Permissions.DataPointAccessTypes.NONE);
ctx.setAttribute("constants.Permissions.DataPointAccessTypes.READ",
diff --git a/src/com/serotonin/mango/rt/dataSource/PollingDataSource.java b/src/com/serotonin/mango/rt/dataSource/PollingDataSource.java
index 4098723397..7d9bcb71cb 100644
--- a/src/com/serotonin/mango/rt/dataSource/PollingDataSource.java
+++ b/src/com/serotonin/mango/rt/dataSource/PollingDataSource.java
@@ -21,6 +21,7 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
import com.serotonin.mango.util.LoggingUtils;
import org.apache.commons.logging.Log;
@@ -49,6 +50,8 @@ abstract public class PollingDataSource extends DataSourceRT implements TimeoutC
private long jobThreadStartTime;
private static volatile boolean markAsTerminating = false;
+ private final AtomicInteger lock = new AtomicInteger(0);
+
public PollingDataSource(DataSourceVO> vo) {
super(vo);
this.vo = vo;
@@ -59,7 +62,7 @@ public void setPollingPeriod(int periodType, int periods, boolean quantize) {
this.quantize = quantize;
}
- public void scheduleTimeout(long fireTime) {
+ public void scheduleTimeout2(long fireTime) {
if(isMarkAsTerminating()) {
return;
}
@@ -86,6 +89,29 @@ public void scheduleTimeout(long fireTime) {
}
}
+ public void scheduleTimeout(long fireTime) {
+
+ if(isMarkAsTerminating()) {
+ return;
+ }
+
+ if(lock.getAndIncrement() == 0) {
+ try {
+ jobThreadStartTime = fireTime;
+ updateChangedPoints();
+ doPoll(fireTime);
+ } finally {
+ lock.set(0);
+ }
+ } else {
+ // There is another poll still running, so abort this one.
+ LOG.warn(vo.getName() + ": poll at " + DateFunctions.getFullSecondTime(fireTime)
+ + " aborted because a previous poll started at "
+ + DateFunctions.getFullSecondTime(jobThreadStartTime) + " is still running");
+ return;
+ }
+ }
+
abstract protected void doPoll(long time);
protected void updateChangedPoints() {
diff --git a/src/com/serotonin/mango/rt/event/type/AuditEventType.java b/src/com/serotonin/mango/rt/event/type/AuditEventType.java
index ae89036781..672fb49c33 100644
--- a/src/com/serotonin/mango/rt/event/type/AuditEventType.java
+++ b/src/com/serotonin/mango/rt/event/type/AuditEventType.java
@@ -22,12 +22,14 @@
import java.util.List;
import java.util.Map;
+import br.org.scadabr.vo.dataSource.opcua.OpcUaDataType;
import com.serotonin.json.JsonException;
import com.serotonin.json.JsonObject;
import com.serotonin.json.JsonReader;
import com.serotonin.json.JsonRemoteEntity;
import com.serotonin.mango.Common;
import com.serotonin.mango.DataTypes;
+import com.serotonin.mango.vo.TimePeriod;
import org.scada_lts.dao.SystemSettingsDAO;
import com.serotonin.mango.rt.event.AlarmLevels;
import com.serotonin.mango.util.ChangeComparable;
@@ -170,6 +172,12 @@ public static void addPeriodMessage(List list, String proper
.getPeriodDescription(periodType, period)));
}
+ public static void addPeriodMessage(List list, String propertyNameKey, TimePeriod periodType,
+ int period) {
+ list.add(new LocalizableMessage("event.audit.property", new LocalizableMessage(propertyNameKey), Common
+ .getPeriodDescription(periodType, period)));
+ }
+
public static void addExportCodeMessage(List list, String propertyNameKey, ExportCodes codes,
int id) {
list.add(new LocalizableMessage("event.audit.property", new LocalizableMessage(propertyNameKey),
@@ -181,6 +189,10 @@ public static void addDataTypeMessage(List list, String prop
.getDataTypeMessage(dataTypeId)));
}
+ public static void addDataTypeMessage(List list, String propertyNameKey, OpcUaDataType dataType) {
+ list.add(new LocalizableMessage("event.audit.property", new LocalizableMessage(propertyNameKey), dataType.name()));
+ }
+
public static void maybeAddPropertyChangeMessage(List list, String propertyNameKey,
int fromValue, int toValue) {
if (fromValue != toValue)
@@ -213,6 +225,13 @@ public static void maybeAddPeriodChangeMessage(List list, St
Common.getPeriodDescription(toPeriodType, toPeriod));
}
+ public static void maybeAddPeriodChangeMessage(List list, String propertyNameKey,
+ TimePeriod fromPeriodType, int fromPeriod, TimePeriod toPeriodType, int toPeriod) {
+ if (fromPeriodType != toPeriodType || fromPeriod != toPeriod)
+ addPropertyChangeMessage(list, propertyNameKey, Common.getPeriodDescription(fromPeriodType, fromPeriod),
+ Common.getPeriodDescription(toPeriodType, toPeriod));
+ }
+
public static void maybeAddExportCodeChangeMessage(List list, String propertyNameKey,
ExportCodes exportCodes, int fromId, int toId) {
if (fromId != toId)
@@ -227,6 +246,13 @@ public static void maybeAddDataTypeChangeMessage(List list,
DataTypes.getDataTypeMessage(toDataTypeId));
}
+ public static void maybeAddObjectChangeMessage(List list, String propertyNameKey,
+ OpcUaDataType fromDataType, OpcUaDataType toDataType) {
+ if (fromDataType != toDataType)
+ addPropertyChangeMessage(list, propertyNameKey, fromDataType,
+ toDataType);
+ }
+
private static LocalizableMessage getBooleanMessage(boolean value) {
if (value)
return new LocalizableMessage("common.true");
diff --git a/src/com/serotonin/mango/rt/event/type/DataSourcePointEventType.java b/src/com/serotonin/mango/rt/event/type/DataSourcePointEventType.java
index f0a04b54b9..bf4a5f1238 100644
--- a/src/com/serotonin/mango/rt/event/type/DataSourcePointEventType.java
+++ b/src/com/serotonin/mango/rt/event/type/DataSourcePointEventType.java
@@ -35,7 +35,7 @@ public class DataSourcePointEventType extends DataSourceEventType {
private DataSourceEventType dataSourceEventType;
private int dataPointId;
- private final int duplicateHandling = DuplicateHandling.IGNORE_SAME_MESSAGE;
+ private final int duplicateHandling = DuplicateHandling.IGNORE;
private final Log LOG = LogFactory.getLog(DataSourcePointEventType.class);
diff --git a/src/com/serotonin/mango/rt/maint/work/PlcConnectionClosingWorkItem.java b/src/com/serotonin/mango/rt/maint/work/PlcConnectionClosingWorkItem.java
new file mode 100644
index 0000000000..39dd3c838e
--- /dev/null
+++ b/src/com/serotonin/mango/rt/maint/work/PlcConnectionClosingWorkItem.java
@@ -0,0 +1,34 @@
+package com.serotonin.mango.rt.maint.work;
+
+import org.apache.plc4x.java.api.PlcConnection;
+import org.apache.plc4x.java.spi.connection.DefaultNettyPlcConnection;
+
+import java.util.concurrent.ExecutionException;
+
+public class PlcConnectionClosingWorkItem extends AbstractBeforeAfterWorkItem {
+
+ private final PlcConnection plcConnection;
+
+ public PlcConnectionClosingWorkItem(PlcConnection plcConnection) {
+ this.plcConnection = plcConnection;
+ }
+
+ @Override
+ public void work() {
+ try {
+ plcConnection.close();
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public WorkItemPriority getPriorityType() {
+ return WorkItemPriority.LOW;
+ }
+
+ @Override
+ public String getDetails() {
+ return "[OPC UA] Closed connection";
+ }
+}
diff --git a/src/com/serotonin/mango/util/LoggingUtils.java b/src/com/serotonin/mango/util/LoggingUtils.java
index 9d2052ea0d..b862cd1a85 100644
--- a/src/com/serotonin/mango/util/LoggingUtils.java
+++ b/src/com/serotonin/mango/util/LoggingUtils.java
@@ -1,5 +1,6 @@
package com.serotonin.mango.util;
+import br.org.scadabr.vo.dataSource.opcua.OpcUaPointLocatorVO;
import br.org.scadabr.vo.scripting.ScriptVO;
import com.serotonin.db.IntValuePair;
import com.serotonin.mango.Common;
@@ -14,6 +15,7 @@
import com.serotonin.mango.vo.DataPointVO;
import com.serotonin.mango.vo.User;
import com.serotonin.mango.vo.dataSource.DataSourceVO;
+import com.serotonin.mango.vo.dataSource.PointLocatorVO;
import com.serotonin.mango.vo.event.EventHandlerVO;
import com.serotonin.mango.vo.event.EventTypeVO;
import com.serotonin.mango.vo.event.PointEventDetectorVO;
@@ -251,6 +253,20 @@ public static String varInfo(IntValuePair pair) {
return MessageFormat.format(info, pair.getValue(), pair.getKey());
}
+ public static String pointLocatorInfo(PointLocatorVO pointLocator) {
+ if(pointLocator == null)
+ return "";
+ if(pointLocator instanceof OpcUaPointLocatorVO) {
+ String info = "locator: {0} (nodeId: {4}, namespaceIndex: {1}, identifier: {2}, identifierType: {3}, dataTypeId: {4}, dataType: {5}, dataType [OPC UA]: {6})";
+ OpcUaPointLocatorVO opcUa = (OpcUaPointLocatorVO) pointLocator;
+ return MessageFormat.format(info, opcUa.getTag(), opcUa.getNodeId(), opcUa.getNamespaceIndex(),
+ opcUa.getIdentifierType(), opcUa.getDataTypeId(), opcUa.getDataTypeMessage().getLocalizedMessage(Common.getBundle()), opcUa.getDataType());
+ }
+ String info = "locator: {0} (dataTypeId: {1}, dataType: {2})";
+ return MessageFormat.format(info, pointLocator.getConfigurationDescription().getLocalizedMessage(Common.getBundle()), pointLocator.getDataTypeId(), pointLocator.getDataTypeMessage().getLocalizedMessage(Common.getBundle()));
+
+ }
+
private static String msg(EventHandlerVO eventHandler) {
return StringUtils.isEmpty(eventHandler.getAlias()) && eventHandler.getMessage() != null ? eventHandler.getMessage().getLocalizedMessage(Common.getBundle()) : eventHandler.getAlias();
}
diff --git a/src/com/serotonin/mango/vo/TimeLongPeriodType.java b/src/com/serotonin/mango/vo/TimeLongPeriodType.java
new file mode 100644
index 0000000000..8e1bbbac31
--- /dev/null
+++ b/src/com/serotonin/mango/vo/TimeLongPeriodType.java
@@ -0,0 +1,80 @@
+package com.serotonin.mango.vo;
+
+import com.serotonin.mango.Common;
+
+import java.util.Map;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+public enum TimeLongPeriodType implements TimePeriod {
+
+ DAYS(Common.TimePeriods.DAYS, "common.tp.days") {
+ @Override
+ public long toMs(long time) {
+ return time * 1000 * 60 * 60 * 24;
+ }
+ },
+ WEEKS(Common.TimePeriods.WEEKS, "common.tp.weeks") {
+ @Override
+ public long toMs(long time) {
+ return time * DAYS.toMs(time);
+ }
+ },
+ MONTHS(Common.TimePeriods.MONTHS, "common.tp.months") {
+ @Override
+ public long toMs(long time) {
+ return time * DAYS.toMs(time) * 30;
+ }
+ },
+ YEARS(Common.TimePeriods.YEARS, "common.tp.years") {
+ @Override
+ public long toMs(long time) {
+ return time * DAYS.toMs(time) * 365;
+ }
+ };
+
+ private final int code;
+ private final String key;
+
+ TimeLongPeriodType(int code, String key) {
+ this.code = code;
+ this.key = key;
+ }
+
+ @Override
+ public int getCode() {
+ return code;
+ }
+
+ @Override
+ public String getKey() {
+ return key;
+ }
+
+ @Override
+ public long toMs(long value) {
+ return value;
+ }
+
+ public static Map codes() {
+ return Stream.of(TimeLongPeriodType.values())
+ .collect(Collectors.toMap(Enum::name, TimeLongPeriodType::getCode));
+ }
+
+ public static TimeLongPeriodType getType(int code) {
+ return Stream.of(TimeLongPeriodType.values())
+ .filter(a -> a.getCode() == code)
+ .findAny()
+ .orElse(TimeLongPeriodType.DAYS);
+ }
+
+ public static TimeLongPeriodType getType(String name) {
+ return Stream.of(TimeLongPeriodType.values())
+ .filter(a -> a.name().equalsIgnoreCase(name))
+ .findAny()
+ .orElse(TimeLongPeriodType.DAYS);
+ }
+}
+
+
+
diff --git a/src/com/serotonin/mango/vo/TimePeriod.java b/src/com/serotonin/mango/vo/TimePeriod.java
new file mode 100644
index 0000000000..c11b1b37ce
--- /dev/null
+++ b/src/com/serotonin/mango/vo/TimePeriod.java
@@ -0,0 +1,7 @@
+package com.serotonin.mango.vo;
+
+public interface TimePeriod {
+ int getCode();
+ long toMs(long value);
+ String getKey();
+}
diff --git a/src/com/serotonin/mango/vo/TimePeriodType.java b/src/com/serotonin/mango/vo/TimePeriodType.java
index a8cc88e8d8..a8590a4d8f 100644
--- a/src/com/serotonin/mango/vo/TimePeriodType.java
+++ b/src/com/serotonin/mango/vo/TimePeriodType.java
@@ -6,38 +6,49 @@
import java.util.stream.Collectors;
import java.util.stream.Stream;
-public enum TimePeriodType {
+public enum TimePeriodType implements TimePeriod {
- MILLISECONDS(Common.TimePeriods.MILLISECONDS),
- SECONDS(Common.TimePeriods.SECONDS) {
+ MILLISECONDS(Common.TimePeriods.MILLISECONDS, "common.tp.milliseconds"),
+ SECONDS(Common.TimePeriods.SECONDS, "common.tp.seconds") {
@Override
public long toMs(long time) {
return time * 1000;
}
},
- MINUTES(Common.TimePeriods.MINUTES) {
+ MINUTES(Common.TimePeriods.MINUTES, "common.tp.minutes") {
@Override
public long toMs(long time) {
return time * 1000 * 60;
}
},
- HOURS(Common.TimePeriods.HOURS) {
+ HOURS(Common.TimePeriods.HOURS, "common.tp.hours") {
@Override
public long toMs(long time) {
return time * 1000 * 60 * 60;
}
};
+ public static final TimePeriodType DEFAULT = TimePeriodType.SECONDS;
+
private final int code;
+ private final String key;
- TimePeriodType(int code) {
+ TimePeriodType(int code, String key) {
this.code = code;
+ this.key = key;
}
+ @Override
public int getCode() {
return code;
}
+ @Override
+ public String getKey() {
+ return key;
+ }
+
+ @Override
public long toMs(long value) {
return value;
}
diff --git a/src/com/serotonin/mango/vo/dataSource/DataSourceVO.java b/src/com/serotonin/mango/vo/dataSource/DataSourceVO.java
index 652c37ee62..291d9b140d 100644
--- a/src/com/serotonin/mango/vo/dataSource/DataSourceVO.java
+++ b/src/com/serotonin/mango/vo/dataSource/DataSourceVO.java
@@ -28,6 +28,7 @@
import br.org.scadabr.vo.dataSource.iec101.IEC101SerialDataSourceVO;
import br.org.scadabr.vo.dataSource.nodaves7.NodaveS7DataSourceVO;
import br.org.scadabr.vo.dataSource.opc.OPCDataSourceVO;
+import br.org.scadabr.vo.dataSource.opcua.OpcUaDataSourceVO;
import cc.radiuino.scadabr.vo.datasource.radiuino.RadiuinoDataSourceVO;
import com.serotonin.ShouldNeverHappenException;
import com.serotonin.json.*;
@@ -317,7 +318,13 @@ public DataSourceVO> createDataSourceVO() {
public DataSourceVO> createDataSourceVO() {
return new MqttDataSourceVO();
}
- },;
+ },
+ OPC_UA(48, "dsEdit.opcua", true) {
+ @Override
+ public DataSourceVO> createDataSourceVO() {
+ return new OpcUaDataSourceVO<>();
+ }
+ };
private Type(int id, String key, boolean display) {
this.id = id;
diff --git a/src/com/serotonin/mango/web/dwr/DataSourceEditDwr.java b/src/com/serotonin/mango/web/dwr/DataSourceEditDwr.java
index 71cbacb9a3..eee5eaf148 100644
--- a/src/com/serotonin/mango/web/dwr/DataSourceEditDwr.java
+++ b/src/com/serotonin/mango/web/dwr/DataSourceEditDwr.java
@@ -44,9 +44,10 @@
import javax.management.remote.JMXServiceURL;
import javax.script.ScriptException;
+import br.org.scadabr.*;
+import br.org.scadabr.vo.dataSource.opcua.*;
import com.serotonin.bacnet4j.type.enumerated.ObjectType;
import com.serotonin.db.KeyValuePair;
-import com.serotonin.mango.util.LoggingUtils;
import com.serotonin.mango.web.dwr.beans.*;
import com.serotonin.modbus4j.SlaveIdLimit255ModbusMaster;
import net.sf.mbus4j.Connection;
@@ -64,9 +65,6 @@
import org.apache.commons.logging.LogFactory;
import org.jinterop.dcom.common.JISystem;
-import br.org.scadabr.OPCItem;
-import br.org.scadabr.OPCUtils;
-import br.org.scadabr.RealOPCMaster;
import br.org.scadabr.vo.dataSource.alpha2.Alpha2DataSourceVO;
import br.org.scadabr.vo.dataSource.alpha2.Alpha2PointLocatorVO;
import br.org.scadabr.vo.dataSource.asciiFile.ASCIIFileDataSourceVO;
@@ -331,23 +329,23 @@ private DataPointVO getPoint(int pointId, DataPointDefaulter defaulter) {
return dp;
}
- private DwrResponseI18n validatePoint(int id, String xid, String name,
- PointLocatorVO locator, DataPointDefaulter defaulter) {
+ private DwrResponseI18n validatePoint(int dataPointId, String dataPointXid, String dataPointName,
+ PointLocatorVO pointLocator, DataPointDefaulter defaulter) {
Permissions.ensureAdmin();
DwrResponseI18n response = new DwrResponseI18n();
- DataPointVO dp = getPoint(id, defaulter);
- dp.setXid(xid);
- dp.setName(name);
- dp.setPointLocator(locator);
+ DataPointVO dp = getPoint(dataPointId, defaulter);
+ dp.setXid(dataPointXid);
+ dp.setName(dataPointName);
+ dp.setPointLocator(pointLocator);
DataPointService dataPointService = new DataPointService();
- validateXid(response, dataPointService::isXidUnique, xid, id);
+ validateXid(response, dataPointService::isXidUnique, dataPointXid, dataPointId);
- if (StringUtils.isEmpty(name))
+ if (StringUtils.isEmpty(dataPointName))
response.addContextualMessage("name", "dsEdit.validate.required");
- locator.validate(response, dp.getId());
+ pointLocator.validate(response, dp.getId());
if (!response.getHasMessages()) {
Common.ctx.getRuntimeManager().saveDataPoint(dp);
@@ -2425,11 +2423,106 @@ public OPCItem validateOPCTag(String tag, String user, String password,
}
- // public void saveOPCTags(OPCItem[] opcItems) {
- // for (int i = 0; i < opcItems.length; i++) {
- // OPCItem opcItem = new OPCItem("", 0, false);
- // }
- // }
+ public DwrResponseI18n saveOpcUaDataSource(OpcUaDataSourceVO> ds) {
+ Permissions.ensureAdmin();
+ return tryDataSourceSave(ds);
+ }
+
+ public DwrResponseI18n saveOpcUaPointLocator(int id, String xid, String name,
+ OpcUaPointLocatorVO locator) {
+ return validatePoint(id, xid, name, locator, null);
+ }
+
+ public LinkedHashSet searchServerOpcUa(OpcUaDataSourceVO> dataSourceVO) {
+
+ Logger log = JISystem.getLogger();
+ log.setLevel(Level.OFF);
+
+ LinkedHashSet serverList = new LinkedHashSet<>();
+ try {
+ OpcUaMaster opcUaMaster = new OpcUaMaster(dataSourceVO);
+ opcUaMaster.init();
+ serverList.add(dataSourceVO.getServerAddress());
+ opcUaMaster.terminate();
+ } catch (Exception e) {
+ LOG.error(e.getMessage(), e);
+ }
+ return serverList;
+ }
+
+ public OpcUaItem findTagOpcUa(OpcUaDataSourceVO> dataSource, String tag, String identifier,
+ OpcUaIdentifierType identifierType, OpcUaDataType dataType) {
+
+ Logger log = JISystem.getLogger();
+ log.setLevel(Level.OFF);
+
+ OpcUaPointLocatorVO pointLocator = new OpcUaPointLocatorVO();
+ pointLocator.setTag(tag);
+ pointLocator.setIdentifier(identifier);
+ pointLocator.setIdentifierType(identifierType);
+ pointLocator.setDataType(dataType);
+
+ OpcUaMaster opcUaMaster;
+ try {
+ opcUaMaster = new OpcUaMaster(dataSource);
+ opcUaMaster.init();
+ } catch (Exception e) {
+ LOG.error(e.getMessage(), e);
+ return new OpcUaItem(false, pointLocator);
+ }
+
+ int namespaceIndex = opcUaMaster.findNamespaceIndex(1000, pointLocator);
+ pointLocator.setNamespaceIndex(namespaceIndex);
+ boolean validated = opcUaMaster.validateTag(pointLocator, tag);
+ pointLocator.setSettable(false);
+
+ try {
+ opcUaMaster.terminate();
+ } catch (Exception e) {
+ LOG.error(e.getMessage(), e);
+ }
+
+ return new OpcUaItem(validated, pointLocator);
+ }
+
+ public DwrResponseI18n saveMultipleOpcUaPointLocator(OpcUaPointLocatorVO[] locators, String context) {
+
+ return validateMultipleOpcUaPoints(locators,
+ context, null);
+ }
+
+ private DwrResponseI18n validateMultipleOpcUaPoints(OpcUaPointLocatorVO[] locators, String context,
+ DataPointDefaulter defaulter) {
+ Permissions.ensureAdmin();
+ DwrResponseI18n response = new DwrResponseI18n();
+ OpcUaDataSourceVO> ds = (OpcUaDataSourceVO>) Common.getUser()
+ .getEditDataSource();
+ if (ds.isNew()) {
+ response.addContextualMessage(context,
+ "dsEdit.opc.validate.dataSourceNotSaved");
+ return response;
+ }
+ for (int i = 0; i < locators.length; i++) {
+ DataPointVO dp = getPoint(Common.NEW_ID, defaulter);
+ String dataPointName = locators[i].getTag();
+ dp.setName(dataPointName);
+ dp.setPointLocator(locators[i]);
+
+ DataPointService dataPointService = new DataPointService();
+ validateXid(response, dataPointService::isXidUnique, dp.getXid(), Common.NEW_ID);
+
+ // locators[i].validate(response);
+ if (!response.getHasMessages()) {
+ Common.ctx.getRuntimeManager().saveDataPoint(dp);
+ response.addData("id", dp.getId());
+ response.addData("points", getPoints());
+ }
+ }
+ return response;
+ }
+
+
+ /// //
public DwrResponseI18n saveMultipleOPCPointLocator(String[] tags,
int[] dataTypes, boolean[] settables, OPCPointLocatorVO[] locators,
diff --git a/src/com/serotonin/mango/web/dwr/KeyStoreTypeConverter.java b/src/com/serotonin/mango/web/dwr/KeyStoreTypeConverter.java
new file mode 100644
index 0000000000..56bbbb807d
--- /dev/null
+++ b/src/com/serotonin/mango/web/dwr/KeyStoreTypeConverter.java
@@ -0,0 +1,27 @@
+package com.serotonin.mango.web.dwr;
+
+import br.org.scadabr.KeyStoreType;
+import org.directwebremoting.convert.EnumConverter;
+import org.directwebremoting.extend.*;
+import org.directwebremoting.util.LocalUtil;
+
+
+public class KeyStoreTypeConverter extends EnumConverter {
+
+ @Override
+ public Object convertInbound(Class paramType, InboundVariable iv, InboundContext inctx) throws MarshallException {
+ String value = LocalUtil.decode(iv.getValue());
+ try {
+ return KeyStoreType.valueOf(value);
+ } catch (Exception var9) {
+ throw new MarshallException(paramType, var9);
+ }
+ }
+
+ @Override
+ public OutboundVariable convertOutbound(Object data, OutboundContext outctx) {
+ KeyStoreType type = (KeyStoreType)data;
+ String name = type.name();
+ return super.convertOutbound(name, outctx);
+ }
+}
diff --git a/src/com/serotonin/mango/web/dwr/OpcUaDataTypeConverter.java b/src/com/serotonin/mango/web/dwr/OpcUaDataTypeConverter.java
new file mode 100644
index 0000000000..ad43958f25
--- /dev/null
+++ b/src/com/serotonin/mango/web/dwr/OpcUaDataTypeConverter.java
@@ -0,0 +1,28 @@
+package com.serotonin.mango.web.dwr;
+
+import br.org.scadabr.vo.dataSource.opcua.OpcUaDataType;
+import org.apache.plc4x.java.opcua.security.SecurityPolicy;
+import org.directwebremoting.convert.EnumConverter;
+import org.directwebremoting.extend.*;
+import org.directwebremoting.util.LocalUtil;
+
+
+public class OpcUaDataTypeConverter extends EnumConverter {
+
+ @Override
+ public Object convertInbound(Class paramType, InboundVariable iv, InboundContext inctx) throws MarshallException {
+ String value = LocalUtil.decode(iv.getValue());
+ try {
+ return OpcUaDataType.valueOf(value);
+ } catch (Exception var9) {
+ throw new MarshallException(paramType, var9);
+ }
+ }
+
+ @Override
+ public OutboundVariable convertOutbound(Object data, OutboundContext outctx) {
+ OpcUaDataType type = (OpcUaDataType)data;
+ String name = type.name();
+ return super.convertOutbound(name, outctx);
+ }
+}
diff --git a/src/com/serotonin/mango/web/dwr/OpcUaIdentifierTypeConverter.java b/src/com/serotonin/mango/web/dwr/OpcUaIdentifierTypeConverter.java
new file mode 100644
index 0000000000..66ab4b6573
--- /dev/null
+++ b/src/com/serotonin/mango/web/dwr/OpcUaIdentifierTypeConverter.java
@@ -0,0 +1,27 @@
+package com.serotonin.mango.web.dwr;
+
+import br.org.scadabr.vo.dataSource.opcua.OpcUaIdentifierType;
+import org.directwebremoting.convert.EnumConverter;
+import org.directwebremoting.extend.*;
+import org.directwebremoting.util.LocalUtil;
+
+
+public class OpcUaIdentifierTypeConverter extends EnumConverter {
+
+ @Override
+ public Object convertInbound(Class paramType, InboundVariable iv, InboundContext inctx) throws MarshallException {
+ String value = LocalUtil.decode(iv.getValue());
+ try {
+ return OpcUaIdentifierType.valueOf(value.toUpperCase());
+ } catch (Exception var9) {
+ throw new MarshallException(paramType, var9);
+ }
+ }
+
+ @Override
+ public OutboundVariable convertOutbound(Object data, OutboundContext outctx) {
+ OpcUaIdentifierType type = (OpcUaIdentifierType)data;
+ String name = type.name();
+ return super.convertOutbound(name, outctx);
+ }
+}
diff --git a/src/com/serotonin/mango/web/dwr/OpcUaMessageSecurityTypeConverter.java b/src/com/serotonin/mango/web/dwr/OpcUaMessageSecurityTypeConverter.java
new file mode 100644
index 0000000000..54e6e3e4a6
--- /dev/null
+++ b/src/com/serotonin/mango/web/dwr/OpcUaMessageSecurityTypeConverter.java
@@ -0,0 +1,27 @@
+package com.serotonin.mango.web.dwr;
+
+import br.org.scadabr.vo.dataSource.opcua.OpcUaMessageSecurityType;
+import org.directwebremoting.convert.EnumConverter;
+import org.directwebremoting.extend.*;
+import org.directwebremoting.util.LocalUtil;
+
+
+public class OpcUaMessageSecurityTypeConverter extends EnumConverter {
+
+ @Override
+ public Object convertInbound(Class paramType, InboundVariable iv, InboundContext inctx) throws MarshallException {
+ String value = LocalUtil.decode(iv.getValue());
+ try {
+ return OpcUaMessageSecurityType.valueOf(value);
+ } catch (Exception var9) {
+ throw new MarshallException(paramType, var9);
+ }
+ }
+
+ @Override
+ public OutboundVariable convertOutbound(Object data, OutboundContext outctx) {
+ OpcUaMessageSecurityType type = (OpcUaMessageSecurityType)data;
+ String name = type.name();
+ return super.convertOutbound(name, outctx);
+ }
+}
diff --git a/src/com/serotonin/mango/web/dwr/OpcUaSecurityPolicyTypeConverter.java b/src/com/serotonin/mango/web/dwr/OpcUaSecurityPolicyTypeConverter.java
new file mode 100644
index 0000000000..b1dcf3fa74
--- /dev/null
+++ b/src/com/serotonin/mango/web/dwr/OpcUaSecurityPolicyTypeConverter.java
@@ -0,0 +1,27 @@
+package com.serotonin.mango.web.dwr;
+
+import br.org.scadabr.vo.dataSource.opcua.OpcUaSecurityPolicyType;
+import org.directwebremoting.convert.EnumConverter;
+import org.directwebremoting.extend.*;
+import org.directwebremoting.util.LocalUtil;
+
+
+public class OpcUaSecurityPolicyTypeConverter extends EnumConverter {
+
+ @Override
+ public Object convertInbound(Class paramType, InboundVariable iv, InboundContext inctx) throws MarshallException {
+ String value = LocalUtil.decode(iv.getValue());
+ try {
+ return OpcUaSecurityPolicyType.valueOf(value.toUpperCase());
+ } catch (Exception var9) {
+ throw new MarshallException(paramType, var9);
+ }
+ }
+
+ @Override
+ public OutboundVariable convertOutbound(Object data, OutboundContext outctx) {
+ OpcUaSecurityPolicyType type = (OpcUaSecurityPolicyType)data;
+ String name = type.name();
+ return super.convertOutbound(name, outctx);
+ }
+}
diff --git a/src/org/scada_lts/cache/UpdateDataSourcesPoints.java b/src/org/scada_lts/cache/UpdateDataSourcesPoints.java
index 27f23c2a86..2a3cf1462e 100644
--- a/src/org/scada_lts/cache/UpdateDataSourcesPoints.java
+++ b/src/org/scada_lts/cache/UpdateDataSourcesPoints.java
@@ -1,5 +1,6 @@
package org.scada_lts.cache;
+import com.serotonin.mango.util.LoggingUtils;
import com.serotonin.mango.vo.DataPointVO;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
@@ -18,10 +19,14 @@ public class UpdateDataSourcesPoints implements StatefulJob{
@Override
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
- LOG.trace("UpdateEventDetectors");
+ try {
+ LOG.trace("UpdateEventDetectors");
List dps = new DataPointDAO().getDataPoints();
Map> dss = DataSourcePointsCache.getInstance().composeCashData(dps);
DataSourcePointsCache.getInstance().setData(dss);
+ } catch (Exception ex) {
+ LOG.error(LoggingUtils.causeInfo(ex), ex);
+ }
}
}
diff --git a/src/org/scada_lts/ds/opcua/TimeoutNettyPlcConnection.java b/src/org/scada_lts/ds/opcua/TimeoutNettyPlcConnection.java
new file mode 100644
index 0000000000..ae93bf5a54
--- /dev/null
+++ b/src/org/scada_lts/ds/opcua/TimeoutNettyPlcConnection.java
@@ -0,0 +1,140 @@
+package org.scada_lts.ds.opcua;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.apache.plc4x.java.api.authentication.PlcAuthentication;
+import org.apache.plc4x.java.api.exceptions.PlcConnectionException;
+import org.apache.plc4x.java.api.exceptions.PlcIoException;
+import org.apache.plc4x.java.api.value.PlcValueHandler;
+import org.apache.plc4x.java.spi.configuration.ConfigurationFactory;
+import org.apache.plc4x.java.spi.configuration.PlcConnectionConfiguration;
+import org.apache.plc4x.java.spi.connection.ChannelFactory;
+import org.apache.plc4x.java.spi.connection.DefaultNettyPlcConnection;
+import org.apache.plc4x.java.spi.connection.PlcTagHandler;
+import org.apache.plc4x.java.spi.connection.ProtocolStackConfigurer;
+import org.apache.plc4x.java.spi.events.CloseConnectionEvent;
+import org.apache.plc4x.java.spi.events.DisconnectEvent;
+import org.apache.plc4x.java.spi.events.DiscoverEvent;
+import org.apache.plc4x.java.spi.optimizer.BaseOptimizer;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.RejectedExecutionException;
+import java.util.concurrent.TimeUnit;
+
+public class TimeoutNettyPlcConnection extends DefaultNettyPlcConnection {
+
+ private static final Logger LOG = LogManager.getLogger(TimeoutNettyPlcConnection.class);
+ private final long connectingTimeout;
+ private final long closingTimeout;
+
+
+ public TimeoutNettyPlcConnection(boolean canPing, boolean canRead, boolean canWrite,
+ boolean canSubscribe, boolean canBrowse, PlcTagHandler tagHandler,
+ PlcValueHandler valueHandler, PlcConnectionConfiguration configuration,
+ ChannelFactory channelFactory, boolean fireDiscoverEvent,
+ boolean awaitSessionSetupComplete, boolean awaitSessionDisconnectComplete,
+ boolean awaitSessionDiscoverComplete, ProtocolStackConfigurer> stackConfigurer,
+ BaseOptimizer optimizer, PlcAuthentication authentication, long connectingTimeout,
+ long closingTimeout) {
+ super(canPing, canRead, canWrite, canSubscribe, canBrowse, tagHandler, valueHandler, configuration, channelFactory, fireDiscoverEvent, awaitSessionSetupComplete, awaitSessionDisconnectComplete, awaitSessionDiscoverComplete, stackConfigurer, optimizer, authentication);
+ this.connectingTimeout = connectingTimeout;
+ this.closingTimeout = closingTimeout;
+ }
+
+ @Override
+ public void connect() throws PlcConnectionException {
+ try {
+ // As we don't just want to wait till the connection is established,
+ // define a future we can use to signal back that the s7 session is
+ // finished initializing.
+ CompletableFuture sessionSetupCompleteFuture = new CompletableFuture<>();
+ CompletableFuture sessionDiscoveredCompleteFuture = new CompletableFuture<>();
+
+ if (channelFactory == null) {
+ throw new PlcConnectionException("No channel factory provided");
+ }
+
+ // Inject the configuration
+ ConfigurationFactory.configure(configuration, channelFactory);
+
+ // Have the channel factory create a new channel instance.
+ // TODO: Why is this code necessary? Discovery should be an API function that is
+ // explicitly called independently from the connection establishment.
+ if (fireDiscoverEvent) {
+ channel = channelFactory.createChannel(getChannelHandler(sessionSetupCompleteFuture, sessionDisconnectCompleteFuture, sessionDiscoveredCompleteFuture));
+ channel.closeFuture().addListener(future -> {
+ if (!sessionDiscoveredCompleteFuture.isDone()) {
+ //Do Nothing
+ try {
+ sessionDiscoveredCompleteFuture.complete(null);
+ } catch (Exception e) {
+ //Do Nothing
+ }
+
+ }
+ });
+ channel.pipeline().fireUserEventTriggered(new DiscoverEvent());
+ }
+ if (awaitSessionDiscoverComplete) {
+ // Wait till the connection is established.
+ sessionDiscoveredCompleteFuture.get();
+ }
+
+ channel = channelFactory.createChannel(getChannelHandler(sessionSetupCompleteFuture, sessionDisconnectCompleteFuture, sessionDiscoveredCompleteFuture));
+ channel.closeFuture().addListener(future -> {
+ if (!sessionSetupCompleteFuture.isDone()) {
+ sessionSetupCompleteFuture.completeExceptionally(
+ new PlcIoException("Connection terminated by remote"));
+ }
+ });
+ // Send an event to the pipeline telling the Protocol filters what's going on.
+ sendChannelCreatedEvent();
+
+ // Wait till the connection is established.
+ if (awaitSessionSetupComplete) {
+ sessionSetupCompleteFuture.get(connectingTimeout, TimeUnit.MILLISECONDS);
+ }
+
+ // Set the connection to "connected"
+ connected = true;
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new PlcConnectionException(e);
+ } catch (Exception e) {
+ throw new PlcConnectionException(e);
+ }
+ }
+
+ @Override
+ public void close() throws PlcConnectionException {
+ LOG.debug("Closing connection to PLC, await for disconnect = {}", this.awaitSessionDisconnectComplete);
+ this.channel.pipeline().fireUserEventTriggered(new DisconnectEvent());
+
+ try {
+ if (this.awaitSessionDisconnectComplete) {
+ this.sessionDisconnectCompleteFuture.get(closingTimeout, TimeUnit.MILLISECONDS);
+ }
+ } catch (Exception var2) {
+ LOG.error("Timeout while trying to close connection");
+ }
+
+ if (this.channel.isOpen()) {
+ try {
+ this.channel.pipeline().fireUserEventTriggered(new CloseConnectionEvent());
+ this.channel.close().awaitUninterruptibly();
+ } catch (RejectedExecutionException ex) {
+ if (this.channel.isOpen()) {
+ throw ex;
+ }
+ }
+ }
+
+ if (!this.sessionDisconnectCompleteFuture.isDone()) {
+ this.sessionDisconnectCompleteFuture.complete(null);
+ }
+
+ this.channelFactory.closeEventLoopForChannel(this.channel);
+ this.channel = null;
+ this.connected = false;
+ }
+}
diff --git a/src/org/scada_lts/ds/opcua/TimeoutOpcuaPlcDriver.java b/src/org/scada_lts/ds/opcua/TimeoutOpcuaPlcDriver.java
new file mode 100644
index 0000000000..707f3610ea
--- /dev/null
+++ b/src/org/scada_lts/ds/opcua/TimeoutOpcuaPlcDriver.java
@@ -0,0 +1,141 @@
+package org.scada_lts.ds.opcua;
+
+import org.apache.plc4x.java.api.PlcConnection;
+import org.apache.plc4x.java.api.authentication.PlcAuthentication;
+import org.apache.plc4x.java.api.exceptions.PlcConnectionException;
+import org.apache.plc4x.java.opcua.OpcuaPlcDriver;
+import org.apache.plc4x.java.opcua.config.OpcuaConfiguration;
+import org.apache.plc4x.java.spi.configuration.ConfigurationFactory;
+import org.apache.plc4x.java.spi.configuration.PlcConnectionConfiguration;
+import org.apache.plc4x.java.spi.configuration.PlcTransportConfiguration;
+import org.apache.plc4x.java.spi.connection.ChannelFactory;
+import org.apache.plc4x.java.spi.transport.Transport;
+import org.apache.plc4x.java.transport.tcp.DefaultTcpTransportConfiguration;
+
+import java.util.ServiceLoader;
+import java.util.regex.Matcher;
+
+import static org.apache.plc4x.java.spi.configuration.ConfigurationFactory.configure;
+
+public class TimeoutOpcuaPlcDriver extends OpcuaPlcDriver {
+
+ public TimeoutOpcuaPlcDriver() {}
+
+ @Override
+ public PlcConnection getConnection(String connectionString, PlcAuthentication authentication) throws PlcConnectionException {
+ ConfigurationFactory configurationFactory = new ConfigurationFactory();
+ // Split up the connection string into its individual segments.
+ Matcher matcher = URI_PATTERN.matcher(connectionString);
+ if (!matcher.matches()) {
+ throw new PlcConnectionException(
+ "Connection string doesn't match the format '{protocol-code}:({transport-code})?//{transport-config}(?{parameter-string)?'");
+ }
+ final String protocolCode = matcher.group("protocolCode");
+ String transportCodeMatch = matcher.group("transportCode");
+ if (transportCodeMatch == null && getMetadata().getDefaultTransportCode().isEmpty()) {
+ throw new PlcConnectionException(
+ "This driver has no default transport and no transport code was provided.");
+ }
+ final String transportCode = (transportCodeMatch != null) ? transportCodeMatch : getMetadata().getDefaultTransportCode().get();
+ final String transportConfig = matcher.group("transportConfig");
+ final String paramString = matcher.group("paramString");
+
+ // Check if the protocol code matches this driver.
+ if (!protocolCode.equals(getProtocolCode())) {
+ // Actually this shouldn't happen as the DriverManager should have not used this driver in the first place.
+ throw new PlcConnectionException(
+ "This driver is not suited to handle this connection string");
+ }
+
+ // Create the configuration object.
+ PlcConnectionConfiguration configuration = configurationFactory
+ .createConfiguration(getConfigurationClass(), protocolCode, transportCode, transportConfig, paramString);
+ if (configuration == null) {
+ throw new PlcConnectionException("Unsupported configuration");
+ }
+
+ // Try to find a suitable transport-type for creating the communication channel.
+ Transport transport = null;
+ ServiceLoader transportLoader = ServiceLoader.load(
+ Transport.class, Thread.currentThread().getContextClassLoader());
+ for (Transport curTransport : transportLoader) {
+ if (curTransport.getTransportCode().equals(transportCode)) {
+ transport = curTransport;
+ break;
+ }
+ }
+ if (transport == null) {
+ throw new PlcConnectionException("Unsupported transport " + transportCode);
+ }
+
+ // Find out the type of the transport configuration.
+ Class extends PlcTransportConfiguration> transportConfigurationType = transport.getTransportConfigType();
+ if (getTransportConfigurationClass(transportCode).isPresent()) {
+ transportConfigurationType = getTransportConfigurationClass(transportCode).get();
+ }
+ // Use the transport configuration type to actually configure the transport instance.
+ PlcTransportConfiguration plcTransportConfiguration = configurationFactory
+ .createTransportConfiguration(transportConfigurationType,
+ protocolCode, transportCode, transportConfig, paramString);
+ configure(plcTransportConfiguration, transport);
+
+ // Create an instance of the communication channel which the driver should use.
+ ChannelFactory channelFactory = transport.createChannelFactory(transportConfig);
+ if (channelFactory == null) {
+ throw new PlcConnectionException("Unable to get channel factory from url " + transportConfig);
+ }
+ configure(configuration, channelFactory);
+
+ // Give drivers the option to customize the channel.
+ initializePipeline(channelFactory);
+
+ // Make the "fire discover event" overridable via system property.
+ boolean fireDiscoverEvent = fireDiscoverEvent();
+ if (System.getProperty(PROPERTY_PLC4X_FORCE_FIRE_DISCOVER_EVENT) != null) {
+ fireDiscoverEvent = Boolean.parseBoolean(System.getProperty(PROPERTY_PLC4X_FORCE_FIRE_DISCOVER_EVENT));
+ }
+
+ // Make the "await setup complete" overridable via system property.
+ boolean awaitSetupComplete = awaitSetupComplete();
+ if (System.getProperty(PROPERTY_PLC4X_FORCE_AWAIT_SETUP_COMPLETE) != null) {
+ awaitSetupComplete = Boolean.parseBoolean(System.getProperty(PROPERTY_PLC4X_FORCE_AWAIT_SETUP_COMPLETE));
+ }
+
+ // Make the "await disconnect complete" overridable via system property.
+ boolean awaitDisconnectComplete = awaitDisconnectComplete();
+ if (System.getProperty(PROPERTY_PLC4X_FORCE_AWAIT_DISCONNECT_COMPLETE) != null) {
+ awaitDisconnectComplete = Boolean.parseBoolean(System.getProperty(PROPERTY_PLC4X_FORCE_AWAIT_DISCONNECT_COMPLETE));
+ }
+
+ // Make the "await disconnect complete" overridable via system property.
+ boolean awaitDiscoverComplete = awaitDiscoverComplete();
+ if (System.getProperty(PROPERTY_PLC4X_FORCE_AWAIT_DISCOVER_COMPLETE) != null) {
+ awaitDiscoverComplete = Boolean.parseBoolean(System.getProperty(PROPERTY_PLC4X_FORCE_AWAIT_DISCOVER_COMPLETE));
+ }
+
+ long connectingTimeout = 4000;
+ long closingTimeout = 4000;
+
+ if(configuration instanceof OpcuaConfiguration) {
+ OpcuaConfiguration tcpTransportConfiguration = (OpcuaConfiguration) configuration;
+ connectingTimeout = tcpTransportConfiguration.getNegotiationTimeout();
+ closingTimeout = tcpTransportConfiguration.getNegotiationTimeout();
+ }
+
+ return new TimeoutNettyPlcConnection(
+ canPing(), canRead(), canWrite(), canSubscribe(), canBrowse(),
+ getTagHandler(),
+ getValueHandler(),
+ configuration,
+ channelFactory,
+ fireDiscoverEvent,
+ awaitSetupComplete,
+ awaitDisconnectComplete,
+ awaitDiscoverComplete,
+ getStackConfigurer(transport),
+ getOptimizer(),
+ authentication,
+ connectingTimeout,
+ closingTimeout);
+ }
+}
diff --git a/src/org/scada_lts/ds/opcua/TimeoutPlcDriverManager.java b/src/org/scada_lts/ds/opcua/TimeoutPlcDriverManager.java
new file mode 100644
index 0000000000..eb4c5fee9d
--- /dev/null
+++ b/src/org/scada_lts/ds/opcua/TimeoutPlcDriverManager.java
@@ -0,0 +1,42 @@
+package org.scada_lts.ds.opcua;
+
+import org.apache.plc4x.java.DefaultPlcDriverManager;
+import org.apache.plc4x.java.api.PlcDriver;
+import org.apache.plc4x.java.api.exceptions.PlcConnectionException;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.ServiceLoader;
+import java.util.Set;
+
+public class TimeoutPlcDriverManager extends DefaultPlcDriverManager {
+
+ private final Map driverMap;
+
+ public TimeoutPlcDriverManager() {
+ driverMap = new HashMap<>();
+ ServiceLoader plcDriverLoader = ServiceLoader.load(TimeoutOpcuaPlcDriver.class, classLoader);
+ for (PlcDriver driver : plcDriverLoader) {
+ if (driverMap.containsKey(driver.getProtocolCode())) {
+ throw new IllegalStateException(
+ "Multiple driver implementations available for protocol code '" +
+ driver.getProtocolCode() + "'");
+ }
+ driverMap.put(driver.getProtocolCode(), driver);
+ }
+ }
+
+ @Override
+ public PlcDriver getDriver(String protocolCode) throws PlcConnectionException {
+ PlcDriver driver = driverMap.get(protocolCode);
+ if (driver == null) {
+ throw new PlcConnectionException("Unable to find driver for protocol '" + protocolCode + "'");
+ }
+ return driver;
+ }
+
+ @Override
+ public Set getProtocolCodes() {
+ return driverMap.keySet();
+ }
+}
diff --git a/webapp-resources/META-INF/services/org.scada_lts.ds.opcua.TimeoutOpcuaPlcDriver b/webapp-resources/META-INF/services/org.scada_lts.ds.opcua.TimeoutOpcuaPlcDriver
new file mode 100644
index 0000000000..a30d23b5dd
--- /dev/null
+++ b/webapp-resources/META-INF/services/org.scada_lts.ds.opcua.TimeoutOpcuaPlcDriver
@@ -0,0 +1 @@
+org.scada_lts.ds.opcua.TimeoutOpcuaPlcDriver
\ No newline at end of file
diff --git a/webapp-resources/log4j2.xml b/webapp-resources/log4j2.xml
index 996e610ff7..f502e07e3e 100644
--- a/webapp-resources/log4j2.xml
+++ b/webapp-resources/log4j2.xml
@@ -30,14 +30,17 @@
ERROR
ERROR
INFO
+ INFO
+ INFO
ERROR
ERROR
ERROR
INFO
- ERROR
+ INFO
INFO
+ INFO
ERROR
@@ -127,6 +130,7 @@
+
@@ -221,6 +225,10 @@
+
+
+
@@ -279,6 +287,10 @@
+
+
+
+
@@ -289,16 +301,6 @@
-
-
-
-
-
-
-
-
-
-
@@ -315,12 +317,19 @@
-
+
+
+
+
-
+
+
+
+
+
diff --git a/webapp-resources/messages_de.properties b/webapp-resources/messages_de.properties
index 9e67a3f58e..80de2fb288 100644
--- a/webapp-resources/messages_de.properties
+++ b/webapp-resources/messages_de.properties
@@ -3368,4 +3368,60 @@ systemSettings.invalidCustomCss=Invalid custom CSS
systemsettings.reports.dataPointExtendedNameLengthLimit=Point name length limit in reports
event.ds.recursiveError=Recursive Error
event.meta.recursiveError=Recursive error in point "{0}": {1}
-validate.cyclicDependency=Cyclic dependency: {0}
\ No newline at end of file
+validate.cyclicDependency=Cyclic dependency: {0}
+dox.opcUaDS=Data Source OPC UA
+dox.opcUaPP=Data Point OPC UA
+dsedit.opcua.rt.addFailed=Addition failed for {0}
+dsedit.opcua.tagName=Tag Name
+dsEdit.opcua=OPC UA
+dsEdit.opcua.TagNotValidated=Tag Not Validated
+dsEdit.opcua.TagValidated=Tag Validated
+dsEdit.opcua.addTags=Add Tags
+dsEdit.opcua.browseTags=Browse Tags
+dsEdit.opcua.creationMode=Creation Mode
+dsEdit.opcua.desc=OPC UA Properties
+dsEdit.opcua.domain=Domain
+dsEdit.opcua.host=Host
+dsEdit.opcua.password=Password
+dsEdit.opcua.refreshServers=Refresh
+dsEdit.opcua.server=Server
+dsEdit.opcua.tag=Tag
+dsEdit.opcua.tagList=Tag List
+dsEdit.opcua.tagName=Tag Name
+dsEdit.opcua.tagsFound=Tags Found
+dsEdit.opcua.user=User
+dsEdit.opcua.validate.dataSourceNotSaved=You must first save the data source
+dsEdit.opcua.validateTag=Validate Tag
+dsEdit.opcua.validation=Validation
+dsEdit.opcua.messageSecurityType=Message Security
+dsEdit.opcua.securityPolicyType=Security Policy
+dsEdit.opcua.serverCertificateFile=Server Certificate File
+dsEdit.opcua.keyStoreType=Key Store Type
+dsEdit.opcua.keyStoreFile=Key Store File
+dsEdit.opcua.keyStorePassword=Key Store Password
+dsEdit.opcua.trustStoreType=Trust Store Type
+dsEdit.opcua.trustStoreFile=Trust Store File
+dsEdit.opcua.trustStorePassword=Trust Store Password
+dsEdit.opcua.channelLifetime=Channel Lifetime
+dsEdit.opcua.sessionTimeout=Session Timeout
+dsEdit.opcua.negotiationTimeout=Negotiation Timeout
+dsEdit.opcua.requestTimeout=Request Timeout
+dsEdit.opcua.defaultTimeout=Default Timeout [TCP]
+dsEdit.opcua.receiveBufferSize=Receiver Buffer Size
+dsEdit.opcua.sendBufferSize=Send Buffer Size
+dsEdit.opcua.maxMessageSize=Max Message Size
+dsEdit.opcua.maxChunkCount=Max Chunk Count
+dsEdit.opcua.keepAlive=Keep Alive [TCP]
+dsEdit.opcua.noDelay=No Delay [TCP]
+dsEdit.opcua.discovery=Discovery
+dsEdit.opcua.serverName=Server Name
+dsEdit.opcua.serverHost=Server Host
+dsEdit.opcua.serverPort=Server Port
+dsEdit.opcua.serverPath=Server Path
+dsEdit.opcua.identifier=Identifier
+dsEdit.opcua.identifierType=Identifier Type
+dsEdit.opcua.dataType=Data Type
+dsEdit.opcua.namespaceIndex=Namespace Index
+dsEdit.opcua.settable=Settable
+dsEdit.opcua.findTag=Find Tag
+dsEdit.opcua.attributes=Attributes
\ No newline at end of file
diff --git a/webapp-resources/messages_en.properties b/webapp-resources/messages_en.properties
index ddb2048e2d..99e0b86cd3 100644
--- a/webapp-resources/messages_en.properties
+++ b/webapp-resources/messages_en.properties
@@ -3371,4 +3371,60 @@ systemSettings.invalidCustomCss=Invalid custom CSS
systemsettings.reports.dataPointExtendedNameLengthLimit=Point name length limit in reports
event.ds.recursiveError=Recursive Error
event.meta.recursiveError=Recursive error in point "{0}": {1}
-validate.cyclicDependency=Cyclic dependency: {0}
\ No newline at end of file
+validate.cyclicDependency=Cyclic dependency: {0}
+dox.opcUaDS=Data Source OPC UA
+dox.opcUaPP=Data Point OPC UA
+dsedit.opcua.rt.addFailed=Addition failed for {0}
+dsedit.opcua.tagName=Tag Name
+dsEdit.opcua=OPC UA
+dsEdit.opcua.TagNotValidated=Tag Not Validated
+dsEdit.opcua.TagValidated=Tag Validated
+dsEdit.opcua.addTags=Add Tags
+dsEdit.opcua.browseTags=Browse Tags
+dsEdit.opcua.creationMode=Creation Mode
+dsEdit.opcua.desc=OPC UA Properties
+dsEdit.opcua.domain=Domain
+dsEdit.opcua.host=Host
+dsEdit.opcua.password=Password
+dsEdit.opcua.refreshServers=Refresh
+dsEdit.opcua.server=Server
+dsEdit.opcua.tag=Tag
+dsEdit.opcua.tagList=Tag List
+dsEdit.opcua.tagName=Tag Name
+dsEdit.opcua.tagsFound=Tags Found
+dsEdit.opcua.user=User
+dsEdit.opcua.validate.dataSourceNotSaved=You must first save the data source
+dsEdit.opcua.validateTag=Validate Tag
+dsEdit.opcua.validation=Validation
+dsEdit.opcua.messageSecurityType=Message Security
+dsEdit.opcua.securityPolicyType=Security Policy
+dsEdit.opcua.serverCertificateFile=Server Certificate File
+dsEdit.opcua.keyStoreType=Key Store Type
+dsEdit.opcua.keyStoreFile=Key Store File
+dsEdit.opcua.keyStorePassword=Key Store Password
+dsEdit.opcua.trustStoreType=Trust Store Type
+dsEdit.opcua.trustStoreFile=Trust Store File
+dsEdit.opcua.trustStorePassword=Trust Store Password
+dsEdit.opcua.channelLifetime=Channel Lifetime
+dsEdit.opcua.sessionTimeout=Session Timeout
+dsEdit.opcua.negotiationTimeout=Negotiation Timeout
+dsEdit.opcua.requestTimeout=Request Timeout
+dsEdit.opcua.defaultTimeout=Default Timeout [TCP]
+dsEdit.opcua.receiveBufferSize=Receiver Buffer Size
+dsEdit.opcua.sendBufferSize=Send Buffer Size
+dsEdit.opcua.maxMessageSize=Max Message Size
+dsEdit.opcua.maxChunkCount=Max Chunk Count
+dsEdit.opcua.keepAlive=Keep Alive [TCP]
+dsEdit.opcua.noDelay=No Delay [TCP]
+dsEdit.opcua.discovery=Discovery
+dsEdit.opcua.serverName=Server Name
+dsEdit.opcua.serverHost=Server Host
+dsEdit.opcua.serverPort=Server Port
+dsEdit.opcua.serverPath=Server Path
+dsEdit.opcua.identifier=Identifier
+dsEdit.opcua.identifierType=Identifier Type
+dsEdit.opcua.dataType=Data Type
+dsEdit.opcua.namespaceIndex=Namespace Index
+dsEdit.opcua.settable=Settable
+dsEdit.opcua.findTag=Find Tag
+dsEdit.opcua.attributes=Attributes
\ No newline at end of file
diff --git a/webapp-resources/messages_es.properties b/webapp-resources/messages_es.properties
index a0e408caba..46ae7bd0cb 100644
--- a/webapp-resources/messages_es.properties
+++ b/webapp-resources/messages_es.properties
@@ -3411,4 +3411,60 @@ systemSettings.invalidCustomCss=Invalid custom CSS
systemsettings.reports.dataPointExtendedNameLengthLimit=Point name length limit in reports
event.ds.recursiveError=Recursive Error
event.meta.recursiveError=Recursive error in point "{0}": {1}
-validate.cyclicDependency=Cyclic dependency: {0}
\ No newline at end of file
+validate.cyclicDependency=Cyclic dependency: {0}
+dox.opcUaDS=Data Source OPC UA
+dox.opcUaPP=Data Point OPC UA
+dsedit.opcua.rt.addFailed=Addition failed for {0}
+dsedit.opcua.tagName=Tag Name
+dsEdit.opcua=OPC UA
+dsEdit.opcua.TagNotValidated=Tag Not Validated
+dsEdit.opcua.TagValidated=Tag Validated
+dsEdit.opcua.addTags=Add Tags
+dsEdit.opcua.browseTags=Browse Tags
+dsEdit.opcua.creationMode=Creation Mode
+dsEdit.opcua.desc=OPC UA Properties
+dsEdit.opcua.domain=Domain
+dsEdit.opcua.host=Host
+dsEdit.opcua.password=Password
+dsEdit.opcua.refreshServers=Refresh
+dsEdit.opcua.server=Server
+dsEdit.opcua.tag=Tag
+dsEdit.opcua.tagList=Tag List
+dsEdit.opcua.tagName=Tag Name
+dsEdit.opcua.tagsFound=Tags Found
+dsEdit.opcua.user=User
+dsEdit.opcua.validate.dataSourceNotSaved=You must first save the data source
+dsEdit.opcua.validateTag=Validate Tag
+dsEdit.opcua.validation=Validation
+dsEdit.opcua.messageSecurityType=Message Security
+dsEdit.opcua.securityPolicyType=Security Policy
+dsEdit.opcua.serverCertificateFile=Server Certificate File
+dsEdit.opcua.keyStoreType=Key Store Type
+dsEdit.opcua.keyStoreFile=Key Store File
+dsEdit.opcua.keyStorePassword=Key Store Password
+dsEdit.opcua.trustStoreType=Trust Store Type
+dsEdit.opcua.trustStoreFile=Trust Store File
+dsEdit.opcua.trustStorePassword=Trust Store Password
+dsEdit.opcua.channelLifetime=Channel Lifetime
+dsEdit.opcua.sessionTimeout=Session Timeout
+dsEdit.opcua.negotiationTimeout=Negotiation Timeout
+dsEdit.opcua.requestTimeout=Request Timeout
+dsEdit.opcua.defaultTimeout=Default Timeout [TCP]
+dsEdit.opcua.receiveBufferSize=Receiver Buffer Size
+dsEdit.opcua.sendBufferSize=Send Buffer Size
+dsEdit.opcua.maxMessageSize=Max Message Size
+dsEdit.opcua.maxChunkCount=Max Chunk Count
+dsEdit.opcua.keepAlive=Keep Alive [TCP]
+dsEdit.opcua.noDelay=No Delay [TCP]
+dsEdit.opcua.discovery=Discovery
+dsEdit.opcua.serverName=Server Name
+dsEdit.opcua.serverHost=Server Host
+dsEdit.opcua.serverPort=Server Port
+dsEdit.opcua.serverPath=Server Path
+dsEdit.opcua.identifier=Identifier
+dsEdit.opcua.identifierType=Identifier Type
+dsEdit.opcua.dataType=Data Type
+dsEdit.opcua.namespaceIndex=Namespace Index
+dsEdit.opcua.settable=Settable
+dsEdit.opcua.findTag=Find Tag
+dsEdit.opcua.attributes=Attributes
\ No newline at end of file
diff --git a/webapp-resources/messages_fi.properties b/webapp-resources/messages_fi.properties
index 96294315e1..45c7631a7a 100644
--- a/webapp-resources/messages_fi.properties
+++ b/webapp-resources/messages_fi.properties
@@ -3496,4 +3496,60 @@ systemSettings.invalidCustomCss=Invalid custom CSS
systemsettings.reports.dataPointExtendedNameLengthLimit=Point name length limit in reports
event.ds.recursiveError=Recursive Error
event.meta.recursiveError=Recursive error in point "{0}": {1}
-validate.cyclicDependency=Cyclic dependency: {0}
\ No newline at end of file
+validate.cyclicDependency=Cyclic dependency: {0}
+dox.opcUaDS=Data Source OPC UA
+dox.opcUaPP=Data Point OPC UA
+dsedit.opcua.rt.addFailed=Addition failed for {0}
+dsedit.opcua.tagName=Tag Tag
+dsEdit.opcua=OPC UA
+dsEdit.opcua.TagNotValidated=Tag Not Validated
+dsEdit.opcua.TagValidated=Tag Validated
+dsEdit.opcua.addTags=Add Tags
+dsEdit.opcua.browseTags=Browse Tags
+dsEdit.opcua.creationMode=Creation Mode
+dsEdit.opcua.desc=OPC UA Properties
+dsEdit.opcua.domain=Domain
+dsEdit.opcua.host=Host
+dsEdit.opcua.password=Password
+dsEdit.opcua.refreshServers=Refresh
+dsEdit.opcua.server=Server
+dsEdit.opcua.tag=Tag
+dsEdit.opcua.tagList=Tag List
+dsEdit.opcua.tagName=Tag Name
+dsEdit.opcua.tagsFound=Tags Found
+dsEdit.opcua.user=User
+dsEdit.opcua.validate.dataSourceNotSaved=You must first save the data source
+dsEdit.opcua.validateTag=Validate Tag
+dsEdit.opcua.validation=Validation
+dsEdit.opcua.messageSecurityType=Message Security
+dsEdit.opcua.securityPolicyType=Security Policy
+dsEdit.opcua.serverCertificateFile=Server Certificate File
+dsEdit.opcua.keyStoreType=Key Store Type
+dsEdit.opcua.keyStoreFile=Key Store File
+dsEdit.opcua.keyStorePassword=Key Store Password
+dsEdit.opcua.trustStoreType=Trust Store Type
+dsEdit.opcua.trustStoreFile=Trust Store File
+dsEdit.opcua.trustStorePassword=Trust Store Password
+dsEdit.opcua.channelLifetime=Channel Lifetime
+dsEdit.opcua.sessionTimeout=Session Timeout
+dsEdit.opcua.negotiationTimeout=Negotiation Timeout
+dsEdit.opcua.requestTimeout=Request Timeout
+dsEdit.opcua.defaultTimeout=Default Timeout [TCP]
+dsEdit.opcua.receiveBufferSize=Receiver Buffer Size
+dsEdit.opcua.sendBufferSize=Send Buffer Size
+dsEdit.opcua.maxMessageSize=Max Message Size
+dsEdit.opcua.maxChunkCount=Max Chunk Count
+dsEdit.opcua.keepAlive=Keep Alive [TCP]
+dsEdit.opcua.noDelay=No Delay [TCP]
+dsEdit.opcua.discovery=Discovery
+dsEdit.opcua.serverName=Server Name
+dsEdit.opcua.serverHost=Server Host
+dsEdit.opcua.serverPort=Server Port
+dsEdit.opcua.serverPath=Server Path
+dsEdit.opcua.identifier=Identifier
+dsEdit.opcua.identifierType=Identifier Type
+dsEdit.opcua.dataType=Data Type
+dsEdit.opcua.namespaceIndex=Namespace Index
+dsEdit.opcua.settable=Settable
+dsEdit.opcua.findTag=Find Tag
+dsEdit.opcua.attributes=Attributes
\ No newline at end of file
diff --git a/webapp-resources/messages_fr.properties b/webapp-resources/messages_fr.properties
index 774be1e8e2..622a68ea81 100644
--- a/webapp-resources/messages_fr.properties
+++ b/webapp-resources/messages_fr.properties
@@ -3365,4 +3365,60 @@ systemSettings.invalidCustomCss=Invalid custom CSS
systemsettings.reports.dataPointExtendedNameLengthLimit=Point name length limit in reports
event.ds.recursiveError=Recursive Error
event.meta.recursiveError=Recursive error in point "{0}": {1}
-validate.cyclicDependency=Cyclic dependency: {0}
\ No newline at end of file
+validate.cyclicDependency=Cyclic dependency: {0}
+dox.opcUaDS=Data Source OPC UA
+dox.opcUaPP=Data Point OPC UA
+dsedit.opcua.rt.addFailed=Addition failed for {0}
+dsedit.opcua.tagName=Tag Tag
+dsEdit.opcua=OPC UA
+dsEdit.opcua.TagNotValidated=Tag Not Validated
+dsEdit.opcua.TagValidated=Tag Validated
+dsEdit.opcua.addTags=Add Tags
+dsEdit.opcua.browseTags=Browse Tags
+dsEdit.opcua.creationMode=Creation Mode
+dsEdit.opcua.desc=OPC UA Properties
+dsEdit.opcua.domain=Domain
+dsEdit.opcua.host=Host
+dsEdit.opcua.password=Password
+dsEdit.opcua.refreshServers=Refresh
+dsEdit.opcua.server=Server
+dsEdit.opcua.tag=Tag
+dsEdit.opcua.tagList=Tag List
+dsEdit.opcua.tagName=Tag Name
+dsEdit.opcua.tagsFound=Tags Found
+dsEdit.opcua.user=User
+dsEdit.opcua.validate.dataSourceNotSaved=You must first save the data source
+dsEdit.opcua.validateTag=Validate Tag
+dsEdit.opcua.validation=Validation
+dsEdit.opcua.messageSecurityType=Message Security
+dsEdit.opcua.securityPolicyType=Security Policy
+dsEdit.opcua.serverCertificateFile=Server Certificate File
+dsEdit.opcua.keyStoreType=Key Store Type
+dsEdit.opcua.keyStoreFile=Key Store File
+dsEdit.opcua.keyStorePassword=Key Store Password
+dsEdit.opcua.trustStoreType=Trust Store Type
+dsEdit.opcua.trustStoreFile=Trust Store File
+dsEdit.opcua.trustStorePassword=Trust Store Password
+dsEdit.opcua.channelLifetime=Channel Lifetime
+dsEdit.opcua.sessionTimeout=Session Timeout
+dsEdit.opcua.negotiationTimeout=Negotiation Timeout
+dsEdit.opcua.requestTimeout=Request Timeout
+dsEdit.opcua.defaultTimeout=Default Timeout [TCP]
+dsEdit.opcua.receiveBufferSize=Receiver Buffer Size
+dsEdit.opcua.sendBufferSize=Send Buffer Size
+dsEdit.opcua.maxMessageSize=Max Message Size
+dsEdit.opcua.maxChunkCount=Max Chunk Count
+dsEdit.opcua.keepAlive=Keep Alive [TCP]
+dsEdit.opcua.noDelay=No Delay [TCP]
+dsEdit.opcua.discovery=Discovery
+dsEdit.opcua.serverName=Server Name
+dsEdit.opcua.serverHost=Server Host
+dsEdit.opcua.serverPort=Server Port
+dsEdit.opcua.serverPath=Server Path
+dsEdit.opcua.identifier=Identifier
+dsEdit.opcua.identifierType=Identifier Type
+dsEdit.opcua.dataType=Data Type
+dsEdit.opcua.namespaceIndex=Namespace Index
+dsEdit.opcua.settable=Settable
+dsEdit.opcua.findTag=Find Tag
+dsEdit.opcua.attributes=Attributes
\ No newline at end of file
diff --git a/webapp-resources/messages_lu.properties b/webapp-resources/messages_lu.properties
index 317f71de78..2821301d19 100644
--- a/webapp-resources/messages_lu.properties
+++ b/webapp-resources/messages_lu.properties
@@ -3384,4 +3384,60 @@ systemSettings.invalidCustomCss=Invalid custom CSS
systemsettings.reports.dataPointExtendedNameLengthLimit=Point name length limit in reports
event.ds.recursiveError=Recursive Error
event.meta.recursiveError=Recursive error in point "{0}": {1}
-validate.cyclicDependency=Cyclic dependency: {0}
\ No newline at end of file
+validate.cyclicDependency=Cyclic dependency: {0}
+dox.opcUaDS=Data Source OPC UA
+dox.opcUaPP=Data Point OPC UA
+dsedit.opcua.rt.addFailed=Addition failed for {0}
+dsedit.opcua.tagName=Tag Tag
+dsEdit.opcua=OPC UA
+dsEdit.opcua.TagNotValidated=Tag Not Validated
+dsEdit.opcua.TagValidated=Tag Validated
+dsEdit.opcua.addTags=Add Tags
+dsEdit.opcua.browseTags=Browse Tags
+dsEdit.opcua.creationMode=Creation Mode
+dsEdit.opcua.desc=OPC UA Properties
+dsEdit.opcua.domain=Domain
+dsEdit.opcua.host=Host
+dsEdit.opcua.password=Password
+dsEdit.opcua.refreshServers=Refresh
+dsEdit.opcua.server=Server
+dsEdit.opcua.tag=Tag
+dsEdit.opcua.tagList=Tag List
+dsEdit.opcua.tagName=Tag Name
+dsEdit.opcua.tagsFound=Tags Found
+dsEdit.opcua.user=User
+dsEdit.opcua.validate.dataSourceNotSaved=You must first save the data source
+dsEdit.opcua.validateTag=Validate Tag
+dsEdit.opcua.validation=Validation
+dsEdit.opcua.messageSecurityType=Message Security
+dsEdit.opcua.securityPolicyType=Security Policy
+dsEdit.opcua.serverCertificateFile=Server Certificate File
+dsEdit.opcua.keyStoreType=Key Store Type
+dsEdit.opcua.keyStoreFile=Key Store File
+dsEdit.opcua.keyStorePassword=Key Store Password
+dsEdit.opcua.trustStoreType=Trust Store Type
+dsEdit.opcua.trustStoreFile=Trust Store File
+dsEdit.opcua.trustStorePassword=Trust Store Password
+dsEdit.opcua.channelLifetime=Channel Lifetime
+dsEdit.opcua.sessionTimeout=Session Timeout
+dsEdit.opcua.negotiationTimeout=Negotiation Timeout
+dsEdit.opcua.requestTimeout=Request Timeout
+dsEdit.opcua.defaultTimeout=Default Timeout [TCP]
+dsEdit.opcua.receiveBufferSize=Receiver Buffer Size
+dsEdit.opcua.sendBufferSize=Send Buffer Size
+dsEdit.opcua.maxMessageSize=Max Message Size
+dsEdit.opcua.maxChunkCount=Max Chunk Count
+dsEdit.opcua.keepAlive=Keep Alive [TCP]
+dsEdit.opcua.noDelay=No Delay [TCP]
+dsEdit.opcua.discovery=Discovery
+dsEdit.opcua.serverName=Server Name
+dsEdit.opcua.serverHost=Server Host
+dsEdit.opcua.serverPort=Server Port
+dsEdit.opcua.serverPath=Server Path
+dsEdit.opcua.identifier=Identifier
+dsEdit.opcua.identifierType=Identifier Type
+dsEdit.opcua.dataType=Data Type
+dsEdit.opcua.namespaceIndex=Namespace Index
+dsEdit.opcua.settable=Settable
+dsEdit.opcua.findTag=Find Tag
+dsEdit.opcua.attributes=Attributes
\ No newline at end of file
diff --git a/webapp-resources/messages_nl.properties b/webapp-resources/messages_nl.properties
index c1a416ea0f..2ea3ddff2a 100644
--- a/webapp-resources/messages_nl.properties
+++ b/webapp-resources/messages_nl.properties
@@ -3486,4 +3486,60 @@ systemSettings.invalidCustomCss=Invalid custom CSS
systemsettings.reports.dataPointExtendedNameLengthLimit=Point name length limit in reports
event.ds.recursiveError=Recursive Error
event.meta.recursiveError=Recursive error in point "{0}": {1}
-validate.cyclicDependency=Cyclic dependency: {0}
\ No newline at end of file
+validate.cyclicDependency=Cyclic dependency: {0}
+dox.opcUaDS=Data Source OPC UA
+dox.opcUaPP=Data Point OPC UA
+dsedit.opcua.rt.addFailed=Addition failed for {0}
+dsedit.opcua.tagName=Tag Tag
+dsEdit.opcua=OPC UA
+dsEdit.opcua.TagNotValidated=Tag Not Validated
+dsEdit.opcua.TagValidated=Tag Validated
+dsEdit.opcua.addTags=Add Tags
+dsEdit.opcua.browseTags=Browse Tags
+dsEdit.opcua.creationMode=Creation Mode
+dsEdit.opcua.desc=OPC UA Properties
+dsEdit.opcua.domain=Domain
+dsEdit.opcua.host=Host
+dsEdit.opcua.password=Password
+dsEdit.opcua.refreshServers=Refresh
+dsEdit.opcua.server=Server
+dsEdit.opcua.tag=Tag
+dsEdit.opcua.tagList=Tag List
+dsEdit.opcua.tagName=Tag Name
+dsEdit.opcua.tagsFound=Tags Found
+dsEdit.opcua.user=User
+dsEdit.opcua.validate.dataSourceNotSaved=You must first save the data source
+dsEdit.opcua.validateTag=Validate Tag
+dsEdit.opcua.validation=Validation
+dsEdit.opcua.messageSecurityType=Message Security
+dsEdit.opcua.securityPolicyType=Security Policy
+dsEdit.opcua.serverCertificateFile=Server Certificate File
+dsEdit.opcua.keyStoreType=Key Store Type
+dsEdit.opcua.keyStoreFile=Key Store File
+dsEdit.opcua.keyStorePassword=Key Store Password
+dsEdit.opcua.trustStoreType=Trust Store Type
+dsEdit.opcua.trustStoreFile=Trust Store File
+dsEdit.opcua.trustStorePassword=Trust Store Password
+dsEdit.opcua.channelLifetime=Channel Lifetime
+dsEdit.opcua.sessionTimeout=Session Timeout
+dsEdit.opcua.negotiationTimeout=Negotiation Timeout
+dsEdit.opcua.requestTimeout=Request Timeout
+dsEdit.opcua.defaultTimeout=Default Timeout [TCP]
+dsEdit.opcua.receiveBufferSize=Receiver Buffer Size
+dsEdit.opcua.sendBufferSize=Send Buffer Size
+dsEdit.opcua.maxMessageSize=Max Message Size
+dsEdit.opcua.maxChunkCount=Max Chunk Count
+dsEdit.opcua.keepAlive=Keep Alive [TCP]
+dsEdit.opcua.noDelay=No Delay [TCP]
+dsEdit.opcua.discovery=Discovery
+dsEdit.opcua.serverName=Server Name
+dsEdit.opcua.serverHost=Server Host
+dsEdit.opcua.serverPort=Server Port
+dsEdit.opcua.serverPath=Server Path
+dsEdit.opcua.identifier=Identifier
+dsEdit.opcua.identifierType=Identifier Type
+dsEdit.opcua.dataType=Data Type
+dsEdit.opcua.namespaceIndex=Namespace Index
+dsEdit.opcua.settable=Settable
+dsEdit.opcua.findTag=Find Tag
+dsEdit.opcua.attributes=Attributes
\ No newline at end of file
diff --git a/webapp-resources/messages_pl.properties b/webapp-resources/messages_pl.properties
index 573d1e0fb3..9f119cc9e8 100644
--- a/webapp-resources/messages_pl.properties
+++ b/webapp-resources/messages_pl.properties
@@ -3508,4 +3508,60 @@ systemSettings.invalidCustomCss=Invalid custom CSS
systemsettings.reports.dataPointExtendedNameLengthLimit=Point name length limit in reports
event.ds.recursiveError=Recursive Error
event.meta.recursiveError=Recursive error in point "{0}": {1}
-validate.cyclicDependency=Cyclic dependency: {0}
\ No newline at end of file
+validate.cyclicDependency=Cyclic dependency: {0}
+dox.opcUaDS=Data Source OPC UA
+dox.opcUaPP=Data Point OPC UA
+dsedit.opcua.rt.addFailed=Addition failed for {0}
+dsedit.opcua.tagName=Tag Tag
+dsEdit.opcua=OPC UA
+dsEdit.opcua.TagNotValidated=Tag Not Validated
+dsEdit.opcua.TagValidated=Tag Validated
+dsEdit.opcua.addTags=Add Tags
+dsEdit.opcua.browseTags=Browse Tags
+dsEdit.opcua.creationMode=Creation Mode
+dsEdit.opcua.desc=OPC UA Properties
+dsEdit.opcua.domain=Domain
+dsEdit.opcua.host=Host
+dsEdit.opcua.password=Password
+dsEdit.opcua.refreshServers=Refresh
+dsEdit.opcua.server=Server
+dsEdit.opcua.tag=Tag
+dsEdit.opcua.tagList=Tag List
+dsEdit.opcua.tagName=Tag Name
+dsEdit.opcua.tagsFound=Tags Found
+dsEdit.opcua.user=User
+dsEdit.opcua.validate.dataSourceNotSaved=You must first save the data source
+dsEdit.opcua.validateTag=Validate Tag
+dsEdit.opcua.validation=Validation
+dsEdit.opcua.messageSecurityType=Message Security
+dsEdit.opcua.securityPolicyType=Security Policy
+dsEdit.opcua.serverCertificateFile=Server Certificate File
+dsEdit.opcua.keyStoreType=Key Store Type
+dsEdit.opcua.keyStoreFile=Key Store File
+dsEdit.opcua.keyStorePassword=Key Store Password
+dsEdit.opcua.trustStoreType=Trust Store Type
+dsEdit.opcua.trustStoreFile=Trust Store File
+dsEdit.opcua.trustStorePassword=Trust Store Password
+dsEdit.opcua.channelLifetime=Channel Lifetime
+dsEdit.opcua.sessionTimeout=Session Timeout
+dsEdit.opcua.negotiationTimeout=Negotiation Timeout
+dsEdit.opcua.requestTimeout=Request Timeout
+dsEdit.opcua.defaultTimeout=Default Timeout [TCP]
+dsEdit.opcua.receiveBufferSize=Receiver Buffer Size
+dsEdit.opcua.sendBufferSize=Send Buffer Size
+dsEdit.opcua.maxMessageSize=Max Message Size
+dsEdit.opcua.maxChunkCount=Max Chunk Count
+dsEdit.opcua.keepAlive=Keep Alive [TCP]
+dsEdit.opcua.noDelay=No Delay [TCP]
+dsEdit.opcua.discovery=Discovery
+dsEdit.opcua.serverName=Server Name
+dsEdit.opcua.serverHost=Server Host
+dsEdit.opcua.serverPort=Server Port
+dsEdit.opcua.serverPath=Server Path
+dsEdit.opcua.identifier=Identifier
+dsEdit.opcua.identifierType=Identifier Type
+dsEdit.opcua.dataType=Data Type
+dsEdit.opcua.namespaceIndex=Namespace Index
+dsEdit.opcua.settable=Settable
+dsEdit.opcua.findTag=Find Tag
+dsEdit.opcua.attributes=Attributes
\ No newline at end of file
diff --git a/webapp-resources/messages_pt.properties b/webapp-resources/messages_pt.properties
index cfc1cfb8d2..e392e6e2ab 100644
--- a/webapp-resources/messages_pt.properties
+++ b/webapp-resources/messages_pt.properties
@@ -3523,4 +3523,60 @@ systemSettings.invalidCustomCss=Invalid custom CSS
systemsettings.reports.dataPointExtendedNameLengthLimit=Point name length limit in reports
event.ds.recursiveError=Recursive Error
event.meta.recursiveError=Recursive error in point "{0}": {1}
-validate.cyclicDependency=Cyclic dependency: {0}
\ No newline at end of file
+validate.cyclicDependency=Cyclic dependency: {0}
+dox.opcUaDS=Data Source OPC UA
+dox.opcUaPP=Data Point OPC UA
+dsedit.opcua.rt.addFailed=Addition failed for {0}
+dsedit.opcua.tagName=Tag Tag
+dsEdit.opcua=OPC UA
+dsEdit.opcua.TagNotValidated=Tag Not Validated
+dsEdit.opcua.TagValidated=Tag Validated
+dsEdit.opcua.addTags=Add Tags
+dsEdit.opcua.browseTags=Browse Tags
+dsEdit.opcua.creationMode=Creation Mode
+dsEdit.opcua.desc=OPC UA Properties
+dsEdit.opcua.domain=Domain
+dsEdit.opcua.host=Host
+dsEdit.opcua.password=Password
+dsEdit.opcua.refreshServers=Refresh
+dsEdit.opcua.server=Server
+dsEdit.opcua.tag=Tag
+dsEdit.opcua.tagList=Tag List
+dsEdit.opcua.tagName=Tag Name
+dsEdit.opcua.tagsFound=Tags Found
+dsEdit.opcua.user=User
+dsEdit.opcua.validate.dataSourceNotSaved=You must first save the data source
+dsEdit.opcua.validateTag=Validate Tag
+dsEdit.opcua.validation=Validation
+dsEdit.opcua.messageSecurityType=Message Security
+dsEdit.opcua.securityPolicyType=Security Policy
+dsEdit.opcua.serverCertificateFile=Server Certificate File
+dsEdit.opcua.keyStoreType=Key Store Type
+dsEdit.opcua.keyStoreFile=Key Store File
+dsEdit.opcua.keyStorePassword=Key Store Password
+dsEdit.opcua.trustStoreType=Trust Store Type
+dsEdit.opcua.trustStoreFile=Trust Store File
+dsEdit.opcua.trustStorePassword=Trust Store Password
+dsEdit.opcua.channelLifetime=Channel Lifetime
+dsEdit.opcua.sessionTimeout=Session Timeout
+dsEdit.opcua.negotiationTimeout=Negotiation Timeout
+dsEdit.opcua.requestTimeout=Request Timeout
+dsEdit.opcua.defaultTimeout=Default Timeout [TCP]
+dsEdit.opcua.receiveBufferSize=Receiver Buffer Size
+dsEdit.opcua.sendBufferSize=Send Buffer Size
+dsEdit.opcua.maxMessageSize=Max Message Size
+dsEdit.opcua.maxChunkCount=Max Chunk Count
+dsEdit.opcua.keepAlive=Keep Alive [TCP]
+dsEdit.opcua.noDelay=No Delay [TCP]
+dsEdit.opcua.discovery=Discovery
+dsEdit.opcua.serverName=Server Name
+dsEdit.opcua.serverHost=Server Host
+dsEdit.opcua.serverPort=Server Port
+dsEdit.opcua.serverPath=Server Path
+dsEdit.opcua.identifier=Identifier
+dsEdit.opcua.identifierType=Identifier Type
+dsEdit.opcua.dataType=Data Type
+dsEdit.opcua.namespaceIndex=Namespace Index
+dsEdit.opcua.settable=Settable
+dsEdit.opcua.findTag=Find Tag
+dsEdit.opcua.attributes=Attributes
\ No newline at end of file
diff --git a/webapp-resources/messages_ru.properties b/webapp-resources/messages_ru.properties
index ea5b9c4d0d..657af961e4 100644
--- a/webapp-resources/messages_ru.properties
+++ b/webapp-resources/messages_ru.properties
@@ -3519,4 +3519,60 @@ systemSettings.invalidCustomCss=Invalid custom CSS
systemsettings.reports.dataPointExtendedNameLengthLimit=Point name length limit in reports
event.ds.recursiveError=Recursive Error
event.meta.recursiveError=Recursive error in point "{0}": {1}
-validate.cyclicDependency=Cyclic dependency: {0}
\ No newline at end of file
+validate.cyclicDependency=Cyclic dependency: {0}
+dox.opcUaDS=Data Source OPC UA
+dox.opcUaPP=Data Point OPC UA
+dsedit.opcua.rt.addFailed=Addition failed for {0}
+dsedit.opcua.tagName=Tag Tag
+dsEdit.opcua=OPC UA
+dsEdit.opcua.TagNotValidated=Tag Not Validated
+dsEdit.opcua.TagValidated=Tag Validated
+dsEdit.opcua.addTags=Add Tags
+dsEdit.opcua.browseTags=Browse Tags
+dsEdit.opcua.creationMode=Creation Mode
+dsEdit.opcua.desc=OPC UA Properties
+dsEdit.opcua.domain=Domain
+dsEdit.opcua.host=Host
+dsEdit.opcua.password=Password
+dsEdit.opcua.refreshServers=Refresh
+dsEdit.opcua.server=Server
+dsEdit.opcua.tag=Tag
+dsEdit.opcua.tagList=Tag List
+dsEdit.opcua.tagName=Tag Name
+dsEdit.opcua.tagsFound=Tags Found
+dsEdit.opcua.user=User
+dsEdit.opcua.validate.dataSourceNotSaved=You must first save the data source
+dsEdit.opcua.validateTag=Validate Tag
+dsEdit.opcua.validation=Validation
+dsEdit.opcua.messageSecurityType=Message Security
+dsEdit.opcua.securityPolicyType=Security Policy
+dsEdit.opcua.serverCertificateFile=Server Certificate File
+dsEdit.opcua.keyStoreType=Key Store Type
+dsEdit.opcua.keyStoreFile=Key Store File
+dsEdit.opcua.keyStorePassword=Key Store Password
+dsEdit.opcua.trustStoreType=Trust Store Type
+dsEdit.opcua.trustStoreFile=Trust Store File
+dsEdit.opcua.trustStorePassword=Trust Store Password
+dsEdit.opcua.channelLifetime=Channel Lifetime
+dsEdit.opcua.sessionTimeout=Session Timeout
+dsEdit.opcua.negotiationTimeout=Negotiation Timeout
+dsEdit.opcua.requestTimeout=Request Timeout
+dsEdit.opcua.defaultTimeout=Default Timeout [TCP]
+dsEdit.opcua.receiveBufferSize=Receiver Buffer Size
+dsEdit.opcua.sendBufferSize=Send Buffer Size
+dsEdit.opcua.maxMessageSize=Max Message Size
+dsEdit.opcua.maxChunkCount=Max Chunk Count
+dsEdit.opcua.keepAlive=Keep Alive [TCP]
+dsEdit.opcua.noDelay=No Delay [TCP]
+dsEdit.opcua.discovery=Discovery
+dsEdit.opcua.serverName=Server Name
+dsEdit.opcua.serverHost=Server Host
+dsEdit.opcua.serverPort=Server Port
+dsEdit.opcua.serverPath=Server Path
+dsEdit.opcua.identifier=Identifier
+dsEdit.opcua.identifierType=Identifier Type
+dsEdit.opcua.dataType=Data Type
+dsEdit.opcua.namespaceIndex=Namespace Index
+dsEdit.opcua.settable=Settable
+dsEdit.opcua.findTag=Find Tag
+dsEdit.opcua.attributes=Attributes
\ No newline at end of file
diff --git a/webapp-resources/messages_zh.properties b/webapp-resources/messages_zh.properties
index 899d2baccf..ea7f9ba487 100644
--- a/webapp-resources/messages_zh.properties
+++ b/webapp-resources/messages_zh.properties
@@ -3471,4 +3471,60 @@ systemSettings.invalidCustomCss=Invalid custom CSS
systemsettings.reports.dataPointExtendedNameLengthLimit=Point name length limit in reports
event.ds.recursiveError=Recursive Error
event.meta.recursiveError=Recursive error in point "{0}": {1}
-validate.cyclicDependency=Cyclic dependency: {0}
\ No newline at end of file
+validate.cyclicDependency=Cyclic dependency: {0}
+dox.opcUaDS=Data Source OPC UA
+dox.opcUaPP=Data Point OPC UA
+dsedit.opcua.rt.addFailed=Addition failed for {0}
+dsedit.opcua.tagName=Tag Tag
+dsEdit.opcua=OPC UA
+dsEdit.opcua.TagNotValidated=Tag Not Validated
+dsEdit.opcua.TagValidated=Tag Validated
+dsEdit.opcua.addTags=Add Tags
+dsEdit.opcua.browseTags=Browse Tags
+dsEdit.opcua.creationMode=Creation Mode
+dsEdit.opcua.desc=OPC UA Properties
+dsEdit.opcua.domain=Domain
+dsEdit.opcua.host=Host
+dsEdit.opcua.password=Password
+dsEdit.opcua.refreshServers=Refresh
+dsEdit.opcua.server=Server
+dsEdit.opcua.tag=Tag
+dsEdit.opcua.tagList=Tag List
+dsEdit.opcua.tagName=Tag Name
+dsEdit.opcua.tagsFound=Tags Found
+dsEdit.opcua.user=User
+dsEdit.opcua.validate.dataSourceNotSaved=You must first save the data source
+dsEdit.opcua.validateTag=Validate Tag
+dsEdit.opcua.validation=Validation
+dsEdit.opcua.messageSecurityType=Message Security
+dsEdit.opcua.securityPolicyType=Security Policy
+dsEdit.opcua.serverCertificateFile=Server Certificate File
+dsEdit.opcua.keyStoreType=Key Store Type
+dsEdit.opcua.keyStoreFile=Key Store File
+dsEdit.opcua.keyStorePassword=Key Store Password
+dsEdit.opcua.trustStoreType=Trust Store Type
+dsEdit.opcua.trustStoreFile=Trust Store File
+dsEdit.opcua.trustStorePassword=Trust Store Password
+dsEdit.opcua.channelLifetime=Channel Lifetime
+dsEdit.opcua.sessionTimeout=Session Timeout
+dsEdit.opcua.negotiationTimeout=Negotiation Timeout
+dsEdit.opcua.requestTimeout=Request Timeout
+dsEdit.opcua.defaultTimeout=Default Timeout [TCP]
+dsEdit.opcua.receiveBufferSize=Receiver Buffer Size
+dsEdit.opcua.sendBufferSize=Send Buffer Size
+dsEdit.opcua.maxMessageSize=Max Message Size
+dsEdit.opcua.maxChunkCount=Max Chunk Count
+dsEdit.opcua.keepAlive=Keep Alive [TCP]
+dsEdit.opcua.noDelay=No Delay [TCP]
+dsEdit.opcua.discovery=Discovery
+dsEdit.opcua.serverName=Server Name
+dsEdit.opcua.serverHost=Server Host
+dsEdit.opcua.serverPort=Server Port
+dsEdit.opcua.serverPath=Server Path
+dsEdit.opcua.identifier=Identifier
+dsEdit.opcua.identifierType=Identifier Type
+dsEdit.opcua.dataType=Data Type
+dsEdit.opcua.namespaceIndex=Namespace Index
+dsEdit.opcua.settable=Settable
+dsEdit.opcua.findTag=Find Tag
+dsEdit.opcua.attributes=Attributes
\ No newline at end of file