# OPC UA Client

This page describes how Connectware can act as an OPC UA client. Connectware can also act as an [OPC UA server](https://docs.cybus.io/2-1-0/documentation/industry-protocol-details/opc-ua/opc-ua-server).

{% hint style="info" %}
For more information on OPC UA, see the [OPC Foundation Reference](https://reference.opcfoundation.org/).
{% endhint %}

## OPC UA Subscriptions

OPC UA distinguishes between sessions, subscriptions, and monitored items.

* Each Connectware connection creates exactly one session in the OPC UA terminology, where Connectware acts as a client towards the server.
* Within one session, the set of data endpoints are grouped into subscriptions. A subscription carries parameters like `publishInterval` and `maxNotificationsPerPublish` that control the data flow between server and client.
* Within each subscription, the actual data endpoints (also called nodes) are subscribed as monitored items. Monitored items are configured by parameters like `nodeId`, `attributeId`, and `samplingInterval`, which control how the server collects the data. Each network request for creating these can create one or multiple monitored items per request.

## Connectware Subscriptions

Connectware abstracts this complexity by combining as many endpoints as possible into the same subscription and grouping monitored items creation into the same request. This is done according to the following criteria:

* Each OPC UA `Cybus::Connection` resource creates exactly one session.
* Within this session, all OPC UA `Cybus::Endpoint` resources with the same `publishInterval` property are combined into one subscription. This holds even across different service commissioning files using inter-service referencing, see [here](https://docs.cybus.io/2-1-0/services/service-commissioning-files/resources#resource-id). Therefore, enabling or disabling additional services with additional endpoints will add or remove those endpoints from the currently available subscription.
* Every OPC UA `Cybus::Endpoint` resource corresponds to one monitored item
* Within one subscription and within the same service commissioning file, all endpoints (monitored items) with the same `samplingInterval` property are combined into one common creation request called `CreateMonitoredItemsGroupRequest`. This request works efficiently even for thousands of monitored items.

### Subscription Limitations

Some OPC UA servers are known for imposing certain limits on the number of monitored items, either in total, or per subscription, or per request.

In particular, an embedded OPC UA server on a Siemens S7-1500 or S7-1200 PLC is known for certain restrictions, see [System limits of OPC UA Server](https://support.industry.siemens.com/cs/document/109755846/what-are-the-system-limits-of-the-opc-ua-server-with-s7-1500-and-s7-1200-?dti=0\&dl=en\&lc=de-DE).

* For example, the maximum number of monitored items within one "create monitored items request" might be limited to 1000 nodes. This limit can be looked up in the OPC UA node with `nodeId` `ns=0;i=11714`. On the Connectware side, this limit must be taken into account by setting the `maxMonitoredItemsPerCall` property in the `Cybus::Connection` resource to the respective value. This ensures that larger requests are split so that all requests stay within this limit.
* Additionally, a maximum number of monitored items in total might be configured in the TIA Portal project. Unfortunately, there is no known way to look up this value except directly in TIA Portal. If such a value is set, Connectware cannot create more endpoints than this limit.

Regarding the created subscriptions: Currently, subscriptions are combined only by the `publishInterval` parameter. The remaining properties related to subscriptions are currently taken only from the first endpoint to be subscribed, while differing settings at subsequent endpoints are ignored. This concerns the following endpoint properties: `requestedLifetimeCount`, `requestedMaxKeepAliveCount`, `maxNotificationsPerPublish`, and `priority`.

### Example

This is an example configuration snippet with three endpoints (without the connection configuration):

{% code lineNumbers="true" expandable="true" %}

```yaml
resources:
  spindleSpeed:
    type: Cybus::Endpoint
    properties:
      protocol: Opcua
      connection: !ref opcuaConnection
      subscribe:
        nodeId: 'ns=1;s=spindleSpeed'
        publishInterval: 1000
        samplingInterval: 100
        maxNotificationsPerPublish: 100
  powerConsumption:
    type: Cybus::Endpoint
    properties:
      protocol: Opcua
      connection: !ref opcuaConnection
      subscribe:
        nodeId: 'ns=1;s=powerConsumption'
        publishInterval: 1000
        maxNotificationsPerPublish: 50
        samplingInterval: 1000
  temperature:
    type: Cybus::Endpoint
    properties:
      protocol: Opcua
      connection: !ref opcuaConnection
      subscribe:
        nodeId: 'ns=1;s=temperature'
        publishInterval: 15000
        samplingInterval: 15000
```

{% endcode %}

In the above example, two subscriptions will be created. One with `publishInterval` set to 1000ms and `maxNotificationsPerPublish` set to 100, and another with `publishInterval` set to 15000ms. The sampling of the individual source values will be set as expected by the specified `samplingInterval` property, but remember that OPC UA does not offer fixed data sampling but rather applies a change-of-value filter to each data point automatically.

[Connection Properties](https://docs.cybus.io/2-1-0/documentation/industry-protocol-details/opc-ua/opc-ua-client/opcuaconnection)

[Endpoint Properties](https://docs.cybus.io/2-1-0/documentation/industry-protocol-details/opc-ua/opc-ua-client/opcuaendpoint)

## Service Commissioning File Example

### Basic Example

This is a simple OPC UA connectivity example that only subscribes to the “Server Status” node of an OPC UA server. The proposed node (endpoint) can also be used if the server would otherwise close the connection, which has been observed for some specific versions of OPC UA servers on a S7 PLC.

The example will need some OPC UA server available in your network:

{% code lineNumbers="true" expandable="true" %}

```yaml
---
description: >

  Simple OPC UA connectivity example

metadata:
  name: Simple OPC UA connectivity
  version: 1.0.0
  icon: https://www.cybus.io/wp-content/uploads/2019/03/Cybus-logo-Claim-lang.svg
  provider: cybus
  homepage: https://www.cybus.io

parameters:
  opcuaHost:
    type: string
    description: OPC-UA Host
    default: 172.17.0.1

  opcuaPort:
    type: integer
    default: 4840

  opcuaUser:
    type: string
    default: ''

  opcuaPass:
    type: string
    default: ''

resources:
  opcuaConnection:
    type: Cybus::Connection
    properties:
      protocol: Opcua
      connection:
        host: !ref opcuaHost
        port: !ref opcuaPort
        username: !ref opcuaUser
        password: !ref opcuaPass

  serverStatusEndpoint:
    type: Cybus::Endpoint
    properties:
      protocol: Opcua
      connection: !ref opcuaConnection
      subscribe:
        nodeId: ns=0;i=2259
        samplingInterval: 2000
```

{% endcode %}

{% file src="<https://2398418777-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FE3kF1al6qtDJ7pi353Uv%2Fuploads%2Fgit-blob-07e20fd4b14fa063b76083563a606b7b1e996220%2Fopcua-simple-example.yml?alt=media>" %}

### Advanced Example

More complex example including an OPC UA server container from public Docker Hub:

{% code lineNumbers="true" expandable="true" %}

```yaml
---
description: >

  Simulated OPC UA server

metadata:
  name: Simulated OPC UA
  version: 1.0.1
  icon: https://www.cybus.io/wp-content/uploads/2019/03/Cybus-logo-Claim-lang.svg
  provider: cybus
  homepage: https://www.cybus.io

parameters:
  opcuaHost:
    type: string
    description: OPC-UA Host
    default: 172.17.0.1

  opcuaPort:
    type: integer
    default: 50000

  opcuaUser:
    type: string
    default: user

  opcuaPass:
    type: string
    default: user

definitions:
  CYBUS_MQTT_ROOT: cybus-factory/opcua

resources:
  machineSimulatorContainer:
    type: Cybus::Container
    properties:
      image: mcr.microsoft.com/iotedge/opc-plc
      ports:
        - !sub '${opcuaPort}:50000/tcp'
      command:
        - --unsecuretransport
        - --autoaccept
        - --defaultuser=user
        - --defaultpassword=user

  opcuaConnection:
    type: Cybus::Connection
    properties:
      protocol: Opcua
      connection:
        host: !ref opcuaHost
        port: !ref opcuaPort
        username: !ref opcuaUser
        password: !ref opcuaPass

  currentTime:
    type: Cybus::Endpoint
    properties:
      protocol: Opcua
      connection: !ref opcuaConnection
      topic: server/status/currenttime
      subscribe:
        nodeId: i=2258

  dipData:
    type: Cybus::Endpoint
    properties:
      protocol: Opcua
      connection: !ref opcuaConnection
      topic: dip-data
      subscribe:
        nodeId: ns=2;s=DipData

  alternatingBool:
    type: Cybus::Endpoint
    properties:
      protocol: Opcua
      connection: !ref opcuaConnection
      topic: alternating-bool
      subscribe:
        nodeId: ns=2;s=AlternatingBoolean

  negativeTrendData:
    type: Cybus::Endpoint
    properties:
      protocol: Opcua
      connection: !ref opcuaConnection
      topic: negative-trend-data
      subscribe:
        nodeId: ns=2;s=NegativeTrendData

  positiveTrendData:
    type: Cybus::Endpoint
    properties:
      protocol: Opcua
      connection: !ref opcuaConnection
      topic: positive-trend-data
      subscribe:
        nodeId: ns=2;s=PositiveTrendData

  randomSignedInt:
    type: Cybus::Endpoint
    properties:
      protocol: Opcua
      connection: !ref opcuaConnection
      topic: random-signed-int32
      subscribe:
        nodeId: ns=2;s=RandomSignedInt32

  randomUnsignedInt:
    type: Cybus::Endpoint
    properties:
      protocol: Opcua
      connection: !ref opcuaConnection
      topic: random-unsigned-int32
      subscribe:
        nodeId: ns=2;s=RandomUnsignedInt32

  spikeData:
    type: Cybus::Endpoint
    properties:
      protocol: Opcua
      connection: !ref opcuaConnection
      topic: spike-data
      subscribe:
        nodeId: ns=2;s=SpikeData

  stepUp:
    type: Cybus::Endpoint
    properties:
      protocol: Opcua
      connection: !ref opcuaConnection
      topic: step-up
      subscribe:
        nodeId: ns=2;s=StepUp
```

{% endcode %}

{% file src="<https://2398418777-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FE3kF1al6qtDJ7pi353Uv%2Fuploads%2Fgit-blob-9375d014c163eada9b1a30e7752500565c83e467%2Fopcua-example.yml?alt=media>" %}

### Example with Method Endpoint

{% code lineNumbers="true" expandable="true" %}

```yaml
description: OPC UA method endpoint

metadata:
  name: opcua-method-endpoint
  version: 1.0.0

resources:
  opcuaConnection:
    type: Cybus::Connection
    properties:
      protocol: Opcua
      connection: # opc.tcp://opcuaserver.com:48010
        host: opcuaserver.com
        port: 48010

  multiplyEndpoint:
    type: Cybus::Endpoint
    properties:
      protocol: Opcua
      connection: !ref opcuaConnection
      publishError: true # will let the endpoint always publish an error object that contains 'message' and error 'code' when an error occurs
      write:
        nodeId: ns=2;s=Demo.Method.Multiply
        nodeType: Method # mark that endpoint as method
```

{% endcode %}

{% file src="<https://2398418777-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FE3kF1al6qtDJ7pi353Uv%2Fuploads%2Fgit-blob-c4f1ddb24c1fb466141681ce8a9ed18f8733a2d2%2Fopcua-client-method-example.yml?alt=media>" %}

## Output Format on Reading

When data is read from OPC UA, the output is provided as a JSON object with value and timestamp.

The given timestamp is the OPC UA “Source Timestamp” which was set on the data source side, see <https://reference.opcfoundation.org/v104/Core/docs/Part4/7.7.3/>

{% code lineNumbers="true" %}

```json
{ "timestamp": "", "value": }
```

{% endcode %}

{% hint style="info" %}
If 64-bit integers are used (which are unsupported in JSON but are supported in JavaScript by the BigInt class), the value is returned as a string containing the decimal number.
{% endhint %}

## Output Format on Writing

### Variables

When data is written to an OPC UA endpoint, you will receive the result of the operation on the `/res` topic like this:

{% code lineNumbers="true" %}

```json
{ "id": "29194", "timestamp": 1629351968526, "result": { "value": 0 } }
```

{% endcode %}

The `id` field is only included if the original request contained an `id`. The value is returned unchanged and can be used to correlate request and response. This behavior applies to both read and write operations.

### Method Calls

Method calls follow the same pattern as variable operations:

1. Write the method call data to the `/set` topic.
2. Receive the result on the `/res` topic.

**Example**

{% code lineNumbers="true" %}

```json
{
  "id": "1234",
  "timestamp": 1738323576339,
  "value": {
    "outputArguments": [
      {
        "value": 42,
        "dataType": "Double",
        "arrayType": "Scalar"
      }
    ]
  }
}
```

{% endcode %}

## Input Format

### Variables

When writing to an OPC UA variable, format the data as a JSON object.

{% code lineNumbers="true" %}

```json
{ "value": }
```

{% endcode %}

### Method Calls

Calling OPC UA methods follows the same pattern as writing to OPC UA variables.

The `value` has to include the input arguments like this. You can optionally include an `id` parameter.

{% code lineNumbers="true" %}

```json
{
  "id": "12345",
  "value": {
    "inputArguments": [
      {
        "value": 24
      },
      {
        "value": 2
      }
    ]
  }
}
```

{% endcode %}

## Complex Data Types

OPC UA nodes can hold simple values (Boolean, Int32, String) or structured values (objects with fields, nested objects, arrays). Connectware maps structured OPC UA values to JSON objects, so you can read, subscribe, and write them using standard `Cybus::Endpoint` resources.

In OPC UA, complex data types are modeled as structured `DataTypes` and exchanged at runtime as `ExtensionObjects`. A complex data type is the logical definition of the data structure, while an `ExtensionObject` is the physical container used to transport that data over the network.

The following operations are supported for complex data types:

* **Read**: Retrieve the current value of a complex variable node.
* **Write**: Update a complex variable node with new structured data.
* **Subscribe**: Receive notifications when a complex variable changes.
* **Method call**: Invoke OPC UA methods with complex data type parameters.

### Prerequisites

Before working with complex data types, ensure the following:

* Identify the `nodeId` of the variable node that exposes the structured value on the OPC UA server.
* For method calls, identify the `nodeId` of the corresponding method node.
* Make sure the OPC UA server already defines the structured DataType for that node.
* Know the exact structure of the DataType as defined on the server, including field names, nesting, scalar versus array definitions, and data types. The JSON payload used in Connectware must match this structure exactly.
* Case sensitivity is important. Use the exact field name as defined on the OPC UA server. For example, if the field name is `MachineName`, then `machineName` will not work (OPC UA standard).

### Example Structure

In the examples below, the complex value has this JSON structure:

{% code lineNumbers="true" %}

```json
{
  "brand": "Cybus",
  "movement": [
    { "backward": false, "forward": false, "left": false, "right": false },
    { "backward": true, "forward": false, "left": true, "right": false }
  ],
  "name": "Robo1",
  "parts": ["arm-1", "arm-2", "arm-3"],
  "version": "8.0.01",
  "sno": 42,
  "mfg": "2026-01-10T10:30:00.000Z"
}
```

{% endcode %}

When writing data, the JSON structure must match the data type defined on the OPC UA server for that node.

#### Service Commissioning File Example

No special configuration is required to work with complex data types. Use standard `Cybus::Endpoint` resources with the `nodeId` of the complex variable node:

{% code lineNumbers="true" %}

```yaml
resources:
  opcuaConnection:
    type: Cybus::Connection
    properties:
      protocol: Opcua
      connection:
        host: !ref opcuaHost
        port: !ref opcuaPort

  complexDataEndpoint:
    type: Cybus::Endpoint
    properties:
      protocol: Opcua
      connection: !ref opcuaConnection
      subscribe:
        nodeId: 'ns=2;s=ComplexDataNode'
```

{% endcode %}

### Subscribing and Receiving Complex Values

1. Create a `Cybus::Endpoint` with the `nodeId` of the complex variable node. No special configuration is required for structured data.
2. Subscribe as usual using the `nodeId`.
3. The structured object appears inside the `value` field of each message.

Incoming message format:

{% code lineNumbers="true" %}

```json
{ "timestamp": , "value": }
```

{% endcode %}

### Writing a Complex Value to a Variable Node

To write complex data to a variable node, send the JSON object as the payload's `value`.

**Example Topic**

`services/robot/writeRobotEndpoint/set`

**Payload**

Put the structured object inside `value`.

{% code lineNumbers="true" %}

```json
{
  "value": {
    "brand": "Cybus",
    "movement": [{ "backward": false, "forward": false, "left": false, "right": false }],
    "name": "Robo-1",
    "parts": ["arm-1"],
    "version": "2.5.01",
    "sno": 784.6,
    "mfg": "2026-01-22T10:30:00.000Z"
  }
}
```

{% endcode %}

**Result**

You will receive the operation result on the `/res` topic, same as writing primitives.

### Calling a Method with a Complex Input Parameter

1. Publish the method call payload to the endpoint `/set` topic.
2. Read the call result on the `/res` topic.

**Example Topic**

`services/robot/setRobotMethod/set`

**Payload**

Include the structured object inside `inputArguments`. In this example, `data` is the input argument name as defined in the OPC UA method. Use the actual input argument name defined in your method.

{% code lineNumbers="true" %}

```json
{
  "value": {
    "inputArguments": [
      {
        "data": {
          "brand": "Cybus",
          "movement": [{ "backward": true, "forward": true, "left": true, "right": true }],
          "name": "Robo-2",
          "parts": ["arm-1", "arm-2"],
          "version": "5.0.01",
          "sno": 546.6,
          "mfg": "2026-01-22T10:30:00.000Z"
        }
      }
    ]
  }
}
```

{% endcode %}

**Result**

Method calls follow the same `/set` and `/res` pattern as variable writes.

### Deadband Limitations

The `deadbandType` and `deadbandValue` options are generally **not applicable** to complex data types.

However, if a primitive numeric field inside a complex structure is accessible via its own variable node (i.e., having its own `nodeId`), you can create a separate endpoint for that specific node and apply deadband settings there.

## Reconnection Behavior

In OPC UA connections, as with any network connections, the connection can be lost. When this happens, Connectware's `OpcuaConnection` will automatically switch into a reconnecting state and repeatedly try to re-establish the connection.

You can control the exact behavior of how often this is attempted by the optional `connectionStrategy` properties, see [Connection Properties](https://docs.cybus.io/2-1-0/documentation/industry-protocol-details/opc-ua/opc-ua-client/opcuaconnection). The `maxDelay` property sets the maximum waiting time (delay) between consecutive retries, in milliseconds. The waiting time starts with the `initialDelay` value and is then increased step by step until reaching the `maxDelay` value.

Example with initially 500ms waiting time, increasing up to 10 seconds:

{% code lineNumbers="true" %}

```yaml
resources:
  myConnection:
    type: Cybus::Connection
    properties:
      protocol: Opcua
      connection:
        host: !ref opcuaHost
        port: !ref opcuaPort
        options:
          connectionStrategy:
            initialDelay: 500
            maxDelay: 10000
```

{% endcode %}

For further details, see the documentation of the internally used package `backoff`: <https://www.npmjs.com/package/backoff>

## Events for OPC UA

Event subscriptions can be created in Connectware by adding the `fields` and `eventTypes` properties to the `Cybus::Endpoint` resource as shown in the example below.

The names for the fields should be qualified names. Often, this will require the field name to also contain a namespace identifier, for example `4:MyQualifiedName.3:SubitemQualifiedName`. Additionally, the field names often need a sub-type, which can be specified using dot notation after the name.

{% code lineNumbers="true" expandable="true" %}

```yaml
---
description: >

  Simple OPC UA connectivity example

metadata:
  name: example for opcua connectivity
  version: 1.0.0
  icon: https://www.cybus.io/wp-content/uploads/2019/03/Cybus-logo-Claim-lang.svg
  provider: cybus
  homepage: https://www.cybus.io

parameters:
  opcuaHost:
    type: string
    description: OPC-UA Host
    default: 172.17.0.1

  opcuaPort:
    type: integer
    default: 4840

  opcuaUser:
    type: string
    default: ''

  opcuaPass:
    type: string
    default: ''

resources:
  opcuaConnection:
    type: Cybus::Connection
    properties:
      protocol: Opcua
      connection:
        host: !ref opcuaHost
        port: !ref opcuaPort
        username: !ref opcuaUser
        password: !ref opcuaPass

  # Listen to all events from the server node
  # A list of eventTypes can be selected
  serverEventsEndpoint:
    type: Cybus::Endpoint
    properties:
      protocol: Opcua
      connection: !ref opcuaConnection
      subscribe:
        nodeId: i=2253
        eventTypes:
          - 'i=85'
        fields:
          - 'EventType'
          - 'ReceiveTime'
          - 'Message'
          - 'SourceName'
          - 'Severity'
```

{% endcode %}

{% file src="<https://2398418777-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FE3kF1al6qtDJ7pi353Uv%2Fuploads%2Fgit-blob-b288d2829b9a3b322efed5e79815e1b631f8128c%2Fopcua-events-example.yml?alt=media>" %}

## Accessing Status Codes for Values

The quality of a data value in OPC UA is represented by predefined status codes. The `statusCode` is transferred to the `$context` object and can be retrieved within the Rule Engine of a `Cybus::Endpoint` via `$context.raw.statusCode` as shown in the example below.

{% code lineNumbers="true" expandable="true" %}

```yaml
description: OPC UA Status Code Example
metadata:
  name: OPC UA Status Code Example
  version: 1.0.0
parameters:
  ipAddress:
    type: string
  port:
    type: integer
    default: 4334
  samplingIntervalMs:
    type: number
    default: 1000
  publishIntervalMs:
    type: number
    default: 1000
resources:
  opcuaConnection:
    type: Cybus::Connection
    properties:
      protocol: Opcua
      connection:
        host: !ref ipAddress
        port: !ref port

  A_000:
    type: Cybus::Endpoint
    properties:
      protocol: Opcua
      connection: !ref opcuaConnection
      subscribe:
        nodeId: ns=1;s=0
        samplingInterval: !ref samplingIntervalMs
        publishInterval: !ref publishIntervalMs
      rules:
        - transform:
            expression: |
              { 
                "statusCode": {
                  "name": $context.raw.statusCode.name,
                  "value": $context.raw.statusCode.value,
                  "description": $context.raw.statusCode.description
                },
                "value": $.value,
                "timestamp": $.timestamp
              }
      topic: A/000
```

{% endcode %}

{% file src="<https://2398418777-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FE3kF1al6qtDJ7pi353Uv%2Fuploads%2Fgit-blob-cd3d1f2c7daf42afae85ba2b2cf1abfe1db31557%2Fopcua-statusCodes-example.yml?alt=media>" %}
