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] IOTransitionTimeoutand 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)
