Error Handling

Errors are reported as exceptions. A call that returns has succeeded, a call that fails raises. There is no return code to inspect and no equivalent of the CTRL function getLastError() that has to be queried after every statement.

An exception can only be raised where the call is made. Results that arrive asynchronously therefore take a different route: a subscription callback receives the error as part of its argument.

Catching Errors

Wrap the calls whose failure the script has to survive.

import logging

from etm.wccoa.exceptions import OaError

logger = logging.getLogger(__name__)

try:
    manager.dp_set_wait("ExampleDP_Arg1.", 42.0)
except OaError:
    logger.exception("could not write the setpoint")

OaError covers the errors reported by WinCC OA itself — an unknown datapoint element, a missing authorization, a system that cannot be reached. OaError describes what it carries.

The exception is raised on the calling thread, so try and except work as usual, including inside a callback.

Bulk Operations

Most calls accept a list of datapoint elements. Such a call does not stop at the first element that fails. It collects the errors of every failing element and reports them together.

One failure raises that error on its own. Several failures raise an ExceptionGroup that holds one exception per failing element, in the order of the request.

from etm.wccoa.exceptions import OaError

try:
    values = manager.dp_get(
        ["ExampleDP_Arg1.", "Missing1.", "Missing2."]
    )
except* OaError as group:
    for exc in group.exceptions:
        logger.error("read failed [%s] %s", exc.code, exc.text)

except* is the matching syntax. It also catches a bare OaError, so one handler covers both cases. A plain except OaError does not, because an ExceptionGroup is not an OaError.

The message of the group names the operation and the number of errors, for example dp_get: multiple errors (2 errors total).

A group can be heterogeneous. An element whose value cannot be converted contributes the exception of that conversion — a ValueError or an OverflowError as described in Errors in Your Own Code — next to the OaError of an element that WinCC OA rejected. Separate handlers keep the two apart:

try:
    manager.dp_set_wait(
        ["ExampleDP_Arg1.", "ExampleDP_Arg2."],
        [42.0, 43.0],
    )
except* OaError as group:
    logger.error("WinCC OA rejected %d elements", len(group.exceptions))
except* (ValueError, OverflowError) as group:
    logger.error("%d values could not be converted", len(group.exceptions))

An error that concerns the call rather than one of its elements is always raised on its own. An empty list, a mismatch between the number of elements and the number of values, or an argument of the wrong type never produces a group.

Writing stays transactional. dp_set() and dp_set_wait() send nothing when any element fails, so a group of errors means that no value was written.

Errors in Callbacks

A subscription does not raise, it reports. event.error is None as long as the subscription works and holds an OaError otherwise. It holds an ExceptionGroup of OaError when WinCC OA reports several errors at once.

def on_change(event):
    if event.error is not None:
        logger.error("subscription failed: %s", event.error)
        return
    for item in event.values:
        print(item.dp_path, item.dp_value)

values is None whenever error is set, so the check has to come first. The callback of a query subscription receives the error as its third parameter — see Query Subscriptions.

An exception that escapes a callback does not stop the subscription. It is logged with level ERROR together with its traceback, and the next update is delivered as usual. Catch it inside the callback where the script has to react to it.

OaError

OaError mirrors the ErrClass structure of the WinCC OA C++ API. Its purpose is to carry an error raised inside WinCC OA — typically reported by the event manager in response to a dp_set() or dp_get() — into Python. The attributes are the fields of that ErrClass.

Attribute Type Content
priority OaErrorPriority FATAL, SEVERE, WARNING or INFO
error_type OaErrorType IMPLEMENTATION, PARAMETRIZATION, SYSTEM, CONTROL or REDUNDANCY
code OaErrorCode or int the error number of WinCC OA
catalog str the message catalogue the number belongs to, empty for the errors of WinCC OA itself
text str the description of the error
time datetime the moment WinCC OA raised the error, as local time

text is the message produced by WinCC OA itself. It is always in English and is not translated into the language of the project. str(exc) returns that text alone, repr(exc) the complete structure. All three enumerations are IntEnum, so a value can be compared and printed by name.

from etm.wccoa.exceptions import OaError

try:
    value = manager.dp_get("ExampleDP_Arg1.")
except OaError as exc:
    logger.error("read failed [%s/%s] %s", exc.priority.name, exc.code, exc.text)

The attributes are diagnostic information. They belong in a log message or in a report to the operator.

Error Codes

OaErrorCode names the values that code can take. It mirrors the error codes of WinCC OA one to one, so a script compares against a name instead of a number.

from etm.wccoa.exceptions import OaError, OaErrorCode

try:
    manager.dp_get("Missing.")
except OaError as exc:
    if exc.code == OaErrorCode.DP_NOT_EXISTENT:
        logger.warning("datapoint has been deleted, skipping")
    else:
        raise

The enumeration covers the errors that WinCC OA reports.

A called CTRL script may use a message catalogue of its own. Its numbers are valid only within that catalogue, so code stays a plain int and catalog names the file; together they identify the error, the way WinCC OA writes it into the log as code/catalog.

A defect in the script is not part of this enumeration — it raises a built-in exception as described in Errors in Your Own Code.

Errors in Your Own Code

Not every exception comes from WinCC OA. Arguments and values are converted into WinCC OA types before a request is sent, and that conversion can fail on its own.

Exception Raised when
ValueError an argument is invalid or a required combination of arguments is incomplete
OverflowError a value does not fit the target type, for example an int beyond the range of INTEGER_VAR
TypeError an argument has a type that cannot be converted

These report a defect in the script. Catching them hides the defect instead of removing it, so let them propagate and correct the call.

Unhandled Exceptions

An exception that no handler catches ends the thread it was raised on. On the main thread it ends the script, and the manager shuts down with it.

The traceback is written to sys.stderr. While the manager runs, sys.stderr is redirected to <project>/log/python<num>.log, so that is where the traceback ends up. A failure before Manager.start(), for example while a module is imported, is written to the stream the process was started with.

In neither case does the traceback pass through the logging module, so it never appears in PVSS_II.log and stays invisible to anyone watching the project log.

Note Wrap the main logic in try and except and log the failure with logger.exception(). The record then reaches PVSS_II.log together with the output of every other manager. See Logging.