Data Access Patterns

Reading, writing and observing datapoint values follows four patterns. Which one to use is a decision about confirmation and timing, not about the value itself. Historical values are a topic of their own and are covered at the end of the page.

All methods shown here are synchronous and thread-safe. They may be called from the script thread, from a thread you started yourself, and from inside a subscription callback — see Threads.

Addressing Datapoint Elements

The addressing scheme itself is the usual WinCC OA one. Two details cause most of the errors in practice.

An element always has to be addressed, never the datapoint alone. For a simple, unstructured datapoint this means the name ends with a dot:

manager.dp_get("ExampleDP_Arg1.")   # correct
manager.dp_get("ExampleDP_Arg1")    # OaError

This applies to every access — reading, writing and subscribing alike.

Config and attribute are completed implicitly when they are not given, and the completion differs by direction:

Access Completed to
Read, subscribe :_online.._value
Write :_original.._value

So dp_get("ExampleDP_Arg1.") reads ExampleDP_Arg1.:_online.._value, while dp_set("ExampleDP_Arg1.", 42.0) writes ExampleDP_Arg1.:_original.._value. Anything else has to be addressed explicitly:

stime = manager.dp_get("ExampleDP_Arg1.:_online.._stime")

Wherever a datapoint element is expected, a str, a DpIdentifier and a DpReference can be used interchangeably — see Data Types.

Most methods accept either a single element or a sequence of elements. The form of the argument decides the form of the result.

Read (Request-Reply)

Read From the Process Image

dp_get() returns the value the Event manager currently holds. It blocks until the value arrives and never talks to the device.

value = manager.dp_get("ExampleDP_Arg1.")

A single element yields a single OaVariant, a sequence yields a list of OaVariant in the order of the request:

values = manager.dp_get([
    "ExampleDP_Arg1.",
    "ExampleDP_Arg2.",
])

Values always arrive as OaVariant — see Data Types for how to work with them.

Read From the Device

Two methods bypass the process image and involve the driver.

Method Value source
dp_get_max_age(age_ms, dpe) the driver value, but only if the held value is older than age_ms
dp_direct_read(dpe, timeout_ms=0) the driver value, bypassing the cache

dp_get_max_age() is only meaningful for elements that have an input or input/output peripheral address. Without such an address it behaves exactly like dp_get() and returns immediately, no matter how old the value is.

Use both deliberately:

  • The call waits for the driver, so it can take considerably longer than dp_get().
  • If the driver cannot deliver a value, the call returns only after the driver timeout. It is configured by [all drivers] IOTransitionTimeout and defaults to 10 seconds.
  • Only some drivers support the single query used by dp_get_max_age() — among them OPC UA Client, Modbus/TCP, S7, S7Plus, SNMP, Ethernet/IP, S-Bus and the OPC drivers.

Queries

dp_query() reads many elements in one request. Which elements and which attributes are selected by a SQL-like query.

result = manager.dp_query(
    "SELECT '_online.._value' FROM 'ExampleDP_Arg*'"
)

The query language is the standard WinCC OA one and is not repeated here; see the query chapter of the WinCC OA online help.

The result is an OaQueryResult — see Data Types.

Write (Request-Reply)

dp_set_wait() writes a value and returns only after the Event manager has processed the write.

manager.dp_set_wait("ExampleDP_Arg1.", 42.0)

Several elements are written in one request by passing two sequences of equal length:

manager.dp_set_wait(
    ["ExampleDP_Arg1.", "ExampleDP_Arg2."],
    [42.0, 43.0],
)

dp_set_timed_wait() does the same but takes a source timestamp as its first argument, for values that were acquired earlier:

import datetime

manager.dp_set_timed_wait(
    datetime.datetime.now() - datetime.timedelta(seconds=30),
    "ExampleDP_Arg1.",
    42.0,
)

Writing and Waiting for a Result

dp_set_and_wait_for_value() writes values and then waits until a condition on other elements is met — the pattern for triggering something and picking up its result.

results = manager.dp_set_and_wait_for_value(
    ["ExampleDP_Arg1."],   # write these values ...
    [123.0],
    ["ExampleDP_Arg2."],   # ... wait until these elements ...
    [123.0],                       # ... hold these values ...
    ["ExampleDP_Arg2."],   # ... then return these elements
    timeout_ms=10000,
)

The call returns when the condition is fulfilled or when the timeout expires. The condition may be fulfilled by the write itself or indirectly, for example by a script or a device reacting to it.

If the timeout expires, the method raises OaError with code OaErrorCode.UNEXPECTEDSTATE (54). With timeout_ms=0 it waits without a time limit.

Fire and Forget

dp_set() and dp_set_timed() send the value and return immediately.

manager.dp_set("ExampleDP_Arg1.", 42.0)

There is no feedback from the Event manager. Only errors that occur on the client side before the request is sent are reported — an element name that cannot be resolved, or sequences of different length. Everything the Event manager finds while processing the request, a missing authorisation for example, never reaches the script.

Use this pattern for cyclic measured values and other high-throughput writes where the individual write does not matter. Do not use it for setpoints, commands or anything the script has to be sure about — use dp_set_wait() there.

Observe (Subscription)

dp_connect() registers a callback that is invoked on every value change until the subscription is cancelled.

def on_change(event):
    for item in event.values:
        print(item.dp_path, item.dp_value)

connect_id = manager.dp_connect(on_change, ["ExampleDP_Arg1."])

The returned connect ID cancels the subscription again:

manager.dp_disconnect(connect_id)

The third parameter answer controls the initial callback. With the default True the callback is invoked once immediately with the current value, so the script starts from a known state. With False the callback is invoked only on the next change.

The callback receives one argument of type ObserveMultiDpValueEventArgs from etm.wccoa.services.events:

Attribute Meaning
error the exception that occurred in the subscription, or None
is_answer True for the initial callback caused by answer=True
is_refresh True if the callback was triggered by a refresh
values tuple of OaDpValueItem, one per subscribed element

Each OaDpValueItem carries dp_path, dp_identifier and dp_value. Check error before touching values — on error values is None.

Callbacks run on a pool thread and may call any API method themselves, including a dp_set() inside a dp_connect() callback.

A script that only subscribes has to keep itself alive, otherwise it stops the manager before the first callback arrives — see Keeping the Script Running.

Query Subscriptions

A query can be subscribed instead of executed once. The callback then fires whenever a value in the query result changes.

def on_query_change(result, update_type, error):
    if error is not None:
        return
    for row_index in range(result.record_count):
        row = result.get_row(row_index)
        print(update_type, row.dp_path, row.values)

connect_id = manager.dp_query_connect_single(
    on_query_change,
    True,
    "SELECT '_online.._value' FROM 'ExampleDP_Arg*'",
)

The callback takes three arguments:

Argument Meaning
result the OaQueryResult of this update, or None on error
update_type OaConnectUpdateType.ANSWER, NORMAL or REFRESH, from etm.wccoa.services.data
error the exception, or None

The second parameter of dp_query_connect_single() is answer and works like the one of dp_connect().

Two variants exist:

Method Content of each callback
dp_query_connect_single only the rows that changed
dp_query_connect_all all subscribed rows, every time

Both accept blocking_time_ms. The default -1 delivers each update immediately. With a value greater than zero, updates are collected for that time and delivered in a single callback, which keeps the callback rate down on fast-changing data.

dp_query_disconnect(connect_id) cancels the subscription.

Historical Data

dp_get_period() reads the values an element had during a time span.

import datetime

end = datetime.datetime.now()
start = end - datetime.timedelta(hours=1)

result = manager.dp_get_period(start, end, ["ExampleDP_Trend1."])

for item in result:
    print(item.dp_path, item.source_time, item.dp_value)

Iterating an OaPeriodResult yields OaDpTimedValueItem objects across all elements. When the elements have to be kept apart, use the series view:

for index in range(result.series_count):
    series = result.get_series(index)
    print(series.dp_path, series.value_count)

The optional count argument limits the number of values per element.

alert_get_period() reads the alert history over the same time span. It takes alert attributes, not plain elements.

items = manager.alert_get_period(start, end, "ExampleDP_AlertHdl1.:_alert_hdl.._value")

for item in items:
    print(item.alarm_time, item.alarm_index, item.dp_value)

Both a single attribute and a list are accepted. The result is a flat list of OaAlertValueItem:

Attribute Meaning
dp_reference, dp_path, dp_identifier address of the element
internal_alarm_time AlertTime — time and counter together
alarm_time the datetime part of internal_alarm_time
alarm_index the counter part — distinguishes alerts at the same timestamp
dp_value the value of the requested alert attribute, or None

A dp_query() with a TIMERANGE clause is a historical query as well. It returns an OaQueryResult like any other query.

Large Result Sets

A historical query can produce more rows than fit into a single reply. dp_query_split() therefore fetches the result in chunks: the first call takes the query, every further call takes the request ID of the previous chunk, until the result reports itself as complete.

chunk = manager.dp_query_split(query)
while True:
    process(chunk.data)
    if chunk.progress == 100:
        break
    chunk = manager.dp_query_split(chunk.request_id)

chunk.data is an OaQueryResult, chunk.progress runs from 0 to 100. A request that is no longer needed is dropped with dp_cancel_split_request(chunk.request_id).

dp_get_period_split() does the same for large historical value result sets. The loop follows the same pattern, with chunk.data being an OaPeriodResult:

chunk = manager.dp_get_period_split(start, end, ["ExampleDP_Trend1."])
while True:
    for item in chunk.data:
        print(item.dp_path, item.source_time, item.dp_value)
    if chunk.progress == 100:
        break
    chunk = manager.dp_get_period_split(chunk.request_id)