/** * Copyright © 2017 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.thingsboard.gateway.extensions.opc; import lombok.extern.slf4j.Slf4j; import org.eclipse.milo.opcua.sdk.client.OpcUaClient; import org.eclipse.milo.opcua.sdk.client.api.config.OpcUaClientConfig; import org.eclipse.milo.opcua.sdk.client.api.identity.IdentityProvider; import org.eclipse.milo.opcua.sdk.client.api.nodes.VariableNode; import org.eclipse.milo.opcua.sdk.client.api.subscriptions.UaMonitoredItem; import org.eclipse.milo.opcua.sdk.client.api.subscriptions.UaSubscription; import org.eclipse.milo.opcua.stack.client.UaTcpStackClient; import org.eclipse.milo.opcua.stack.core.AttributeId; import org.eclipse.milo.opcua.stack.core.Identifiers; import org.eclipse.milo.opcua.stack.core.security.SecurityPolicy; import org.eclipse.milo.opcua.stack.core.types.builtin.DataValue; import org.eclipse.milo.opcua.stack.core.types.builtin.LocalizedText; import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId; import org.eclipse.milo.opcua.stack.core.types.builtin.QualifiedName; import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UInteger; import org.eclipse.milo.opcua.stack.core.types.enumerated.*; import org.eclipse.milo.opcua.stack.core.types.structured.*; import org.thingsboard.gateway.extensions.opc.conf.OpcUaServerConfiguration; import org.thingsboard.gateway.extensions.opc.conf.mapping.DeviceMapping; import org.thingsboard.gateway.extensions.opc.scan.OpcUaNode; import org.thingsboard.gateway.service.GatewayService; import org.thingsboard.gateway.util.CertificateInfo; import org.thingsboard.gateway.util.ConfigurationTools; import org.thingsboard.server.common.data.kv.KvEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; import java.util.*; import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicLong; import java.util.function.BiConsumer; import java.util.function.Function; import java.util.regex.Pattern; import java.util.stream.Collectors; import static org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.Unsigned.uint; import static org.eclipse.milo.opcua.stack.core.util.ConversionUtil.toList; /** * Created by ashvayka on 16.01.17. */ @Slf4j public class OpcUaServerMonitor { private final GatewayService gateway; private final OpcUaServerConfiguration configuration; private OpcUaClient client; private UaSubscription subscription; private Map<NodeId, OpcUaDevice> devices; private Map<NodeId, List<OpcUaDevice>> devicesByTags; private Map<Pattern, DeviceMapping> mappings; private ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(); private final AtomicLong clientHandles = new AtomicLong(1L); public OpcUaServerMonitor(GatewayService gateway, OpcUaServerConfiguration configuration) { this.gateway = gateway; this.configuration = configuration; this.devices = new HashMap<>(); this.devicesByTags = new HashMap<>(); this.mappings = configuration.getMapping().stream().collect(Collectors.toMap(m -> Pattern.compile(m.getDeviceNodePattern()), Function.identity())); } public void connect() { try { log.info("Initializing OPC-UA server connection to [{}:{}]!", configuration.getHost(), configuration.getPort()); CertificateInfo certificate = ConfigurationTools.loadCertificate(configuration.getKeystore()); SecurityPolicy securityPolicy = SecurityPolicy.valueOf(configuration.getSecurity()); IdentityProvider identityProvider = configuration.getIdentity().toProvider(); EndpointDescription[] endpoints = UaTcpStackClient.getEndpoints("opc.tcp://" + configuration.getHost() + ":" + configuration.getPort() + "/").get(); EndpointDescription endpoint = Arrays.stream(endpoints) .filter(e -> e.getSecurityPolicyUri().equals(securityPolicy.getSecurityPolicyUri())) .findFirst().orElseThrow(() -> new Exception("no desired endpoints returned")); OpcUaClientConfig config = OpcUaClientConfig.builder() .setApplicationName(LocalizedText.english(configuration.getApplicationName())) .setApplicationUri(configuration.getApplicationUri()) .setCertificate(certificate.getCertificate()) .setKeyPair(certificate.getKeyPair()) .setEndpoint(endpoint) .setIdentityProvider(identityProvider) .setRequestTimeout(uint(configuration.getTimeoutInMillis())) .build(); client = new OpcUaClient(config); client.connect().get(); subscription = client.getSubscriptionManager().createSubscription(1000.0).get(); scanForDevices(); } catch (Exception e) { log.error("OPC-UA server connection failed!", e); throw new RuntimeException("OPC-UA server connection failed!", e); } } public void disconnect() { if (client != null) { log.info("Disconnecting from OPC-UA server!"); try { client.disconnect().get(10, TimeUnit.SECONDS); log.info("Disconnected from OPC-UA server!"); } catch (InterruptedException | ExecutionException | TimeoutException e) { log.info("Failed to disconnect from OPC-UA server!"); } } } public void scanForDevices() { try { long startTs = System.currentTimeMillis(); scanForDevices(new OpcUaNode(Identifiers.RootFolder, "")); log.info("Device scan cycle completed in {} ms", (System.currentTimeMillis() - startTs)); List<OpcUaDevice> deleted = devices.entrySet().stream().filter(kv -> kv.getValue().getScanTs() < startTs).map(kv -> kv.getValue()).collect(Collectors.toList()); if (deleted.size() > 0) { log.info("Devices {} are no longer available", deleted); } deleted.forEach(devices::remove); deleted.stream().map(OpcUaDevice::getDeviceName).forEach(gateway::onDeviceDisconnect); } catch (Exception e) { log.warn("Periodic device scan failed!", e); } log.info("Scheduling next scan in {} seconds!", configuration.getScanPeriodInSeconds()); executor.schedule(() -> { scanForDevices(); }, configuration.getScanPeriodInSeconds(), TimeUnit.SECONDS); } private void scanForDevices(OpcUaNode node) { log.trace("Scanning node: {}", node); List<DeviceMapping> matchedMappings = mappings.entrySet().stream() .filter(mappingEntry -> mappingEntry.getKey().matcher(node.getName()).matches()) .map(m -> m.getValue()).collect(Collectors.toList()); matchedMappings.forEach(m -> { try { scanDevice(node, m); } catch (Exception e) { log.error("Failed to scan device: {}", node.getName(), e); } }); try { BrowseResult browseResult = client.browse(getBrowseDescription(node.getNodeId())).get(); List<ReferenceDescription> references = toList(browseResult.getReferences()); for (ReferenceDescription rd : references) { NodeId nodeId; if (rd.getNodeId().isLocal()) { nodeId = rd.getNodeId().local().get(); } else { log.trace("Ignoring remote node: {}", rd.getNodeId()); continue; } OpcUaNode childNode = new OpcUaNode(node, nodeId, rd.getBrowseName().getName()); // recursively browse to children scanForDevices(childNode); } } catch (InterruptedException | ExecutionException e) { log.error("Browsing nodeId={} failed: {}", node, e.getMessage(), e); } } private void scanDevice(OpcUaNode node, DeviceMapping m) throws Exception { log.debug("Scanning device node: {}", node); Set<String> tags = m.getAllTags(); log.debug("Scanning node hierarchy for tags: {}", tags); Map<String, NodeId> tagMap = lookupTags(node.getNodeId(), node.getName(), tags); log.debug("Scanned {} tags out of {}", tagMap.size(), tags.size()); OpcUaDevice device; if (devices.containsKey(node.getNodeId())) { device = devices.get(node.getNodeId()); } else { device = new OpcUaDevice(node.getNodeId(), m); devices.put(node.getNodeId(), device); Map<String, NodeId> deviceNameTags = new HashMap<>(); for (String tag : m.getDeviceNameTags()) { NodeId tagNode = tagMap.get(tag); if (tagNode == null) { log.error("Not enough info to populate device id for node [{}]. Tag [{}] is missing!", node.getName(), tag); throw new IllegalArgumentException("Not enough info to populate device id. Tag: [" + tag + "] is missing!"); } else { deviceNameTags.put(tag, tagNode); } } device.calculateDeviceName(readTags(deviceNameTags)); gateway.onDeviceConnect(device.getDeviceName()); } device.updateScanTs(); Map<String, NodeId> newTags = device.registerTags(tagMap); if (newTags.size() > 0) { for (NodeId tagId : newTags.values()) { devicesByTags.putIfAbsent(tagId, new ArrayList<>()); devicesByTags.get(tagId).add(device); } log.debug("Going to subscribe to tags: {}", newTags); subscribeToTags(newTags); } } private void subscribeToTags(Map<String, NodeId> newTags) throws InterruptedException, ExecutionException { List<MonitoredItemCreateRequest> requests = new ArrayList<>(); for (Map.Entry<String, NodeId> kv : newTags.entrySet()) { // subscribe to the Value attribute of the server's CurrentTime node ReadValueId readValueId = new ReadValueId( kv.getValue(), AttributeId.Value.uid(), null, QualifiedName.NULL_VALUE); // important: client handle must be unique per item UInteger clientHandle = uint(clientHandles.getAndIncrement()); MonitoringParameters parameters = new MonitoringParameters( clientHandle, 1000.0, // sampling interval null, // filter, null means use default uint(10), // queue size true // discard oldest ); requests.add(new MonitoredItemCreateRequest( readValueId, MonitoringMode.Reporting, parameters)); } BiConsumer<UaMonitoredItem, Integer> onItemCreated = (item, id) -> item.setValueConsumer(this::onSubscriptionValue); List<UaMonitoredItem> items = subscription.createMonitoredItems( TimestampsToReturn.Both, requests, onItemCreated ).get(); for (UaMonitoredItem item : items) { if (item.getStatusCode().isGood()) { log.trace("Monitoring Item created for nodeId={}", item.getReadValueId().getNodeId()); } else { log.warn("Failed to create item for nodeId={} (status={})", item.getReadValueId().getNodeId(), item.getStatusCode()); } } } private void onSubscriptionValue(UaMonitoredItem item, DataValue dataValue) { log.debug("Subscription value received: item={}, value={}", item.getReadValueId().getNodeId(), dataValue.getValue()); NodeId tagId = item.getReadValueId().getNodeId(); devicesByTags.getOrDefault(tagId, Collections.emptyList()).forEach( device -> { device.updateTag(tagId, dataValue); List<KvEntry> attributes = device.getAffectedAttributes(tagId, dataValue); if (attributes.size() > 0) { gateway.onDeviceAttributesUpdate(device.getDeviceName(), attributes); } List<TsKvEntry> timeseries = device.getAffectedTimeseries(tagId, dataValue); if (timeseries.size() > 0) { gateway.onDeviceTelemetry(device.getDeviceName(), timeseries); } } ); } private Map<String, String> readTags(Map<String, NodeId> tags) throws ExecutionException, InterruptedException { Map<String, CompletableFuture<DataValue>> dataFutures = new HashMap<>(); for (Map.Entry<String, NodeId> kv : tags.entrySet()) { VariableNode node = client.getAddressSpace().createVariableNode(kv.getValue()); dataFutures.put(kv.getKey(), node.readValue()); } Map<String, String> result = new HashMap<>(); for (Map.Entry<String, CompletableFuture<DataValue>> kv : dataFutures.entrySet()) { String tag = kv.getKey(); DataValue value = kv.getValue().get(); result.put(tag, value.getValue().getValue().toString()); } return result; } private Map<String, NodeId> lookupTags(NodeId nodeId, String deviceNodeName, Set<String> tags) { Map<String, NodeId> values = new HashMap<>(); try { BrowseResult browseResult = client.browse(getBrowseDescription(nodeId)).get(); List<ReferenceDescription> references = toList(browseResult.getReferences()); for (ReferenceDescription rd : references) { NodeId childId; if (rd.getNodeId().isLocal()) { childId = rd.getNodeId().local().get(); } else { log.trace("Ignoring remote node: {}", rd.getNodeId()); continue; } String browseName = rd.getBrowseName().getName(); String name = browseName.substring(deviceNodeName.length() + 1); // 1 is for extra . if (tags.contains(name)) { values.put(name, childId); } // recursively browse to children values.putAll(lookupTags(childId, deviceNodeName, tags)); } } catch (InterruptedException | ExecutionException e) { log.error("Browsing nodeId={} failed: {}", nodeId, e.getMessage(), e); } return values; } private BrowseDescription getBrowseDescription(NodeId nodeId) { return new BrowseDescription( nodeId, BrowseDirection.Forward, Identifiers.References, true, uint(NodeClass.Object.getValue() | NodeClass.Variable.getValue()), uint(BrowseResultMask.All.getValue()) ); } }