A Minimal OPC-UA Client#

In this section we will build a client which reads and writes data of the server created in the last section and calls the method which the server provides. Running the client code requires a running server of course, so open a new terminal and run python server.py to start it. The certificates and the user alice were set up in A Minimal OPC-UA Server.

client.py#
import asyncio

from enginir_opcua import Client


async def main() -> None:
    client = Client.quickstart("opc.tcp://127.0.0.1:4840/enginir-opcua/server/", pki="cli/pki", username="alice")
    async with client:
        idx = await client.get_namespace_index("urn:example:minimal")
        my_variable = await client.nodes.objects.get_child([f"{idx}:MyObject", f"{idx}:MyVariable"])
        print("MyVariable:", await my_variable.read_value())
        await my_variable.write_value(3.9)
        print("MyVariable after the write:", await my_variable.read_value())
        result = await client.nodes.objects.call_method(f"{idx}:multiply", 3, 7)
        print("multiply(3, 7) =", result)


asyncio.run(main())

Set the password of alice in the environment variable ENGINIR_OPCUA_PASSWORD before you run it; Client.quickstart() reads it when no password is given, so it never appears in the source. The ApplicationUri comes from the certificate that enginir-opcua init client created.

Connecting to the server#

Client.quickstart() creates a Client with the secure profile: Basic256Sha256 with SignAndEncrypt. The client’s PKI directory has to hold the server’s certificate in trusted/certs (enginir-opcua pair did that), or the connection fails with an error that names the thumbprint and the enginir-opcua trust command for each side. The client supports the same async context manager as the server, which opens and closes the connection for us. Without username the session is anonymous and read-only.

A plain Client(url) has no security choice and refuses to connect. Use Client.quickstart, call set_security(), or write Client(url, allow_no_security=True) for a server on a closed test network; the client then warns that the channel is neither signed nor encrypted. See Security Profiles.

Getting the namespace#

As all our custom objects live in a custom namespace, we need the namespace index to address them. This is done with get_namespace_index(). The list of all namespaces of the server is returned by get_namespace_array().

Read / Write Variables#

To read or write a variable of an object, we first need its Node. The get_child() method of the Objects node (which is just a regular node) turns the known path into a Node.

Note

Using get_child() will perform a server request in the background to resolve the path to a node. Extensive usage of this method can create a lot of network traffic which is not strictly required if the node id is known. If you know the node id it’s better to use the get_node() method of the client. For example client.get_node("ns=2;i=2") or client.get_node(ua.NodeId(2, 2)) could be used in the example. Note that the latter call is not async!

Once we have our node object, the variable value can be read or written directly using the read_value() and write_value() methods. The read method automatically transforms the opc-ua type to a python type but the read_data_value() method can be used if the original type of the variable is of interest. The write interface is built flexible and a Variant is also accepted to specify the exact type to be used. A write needs the role user; an anonymous client is refused with BadUserAccessDenied (see 02_users_and_roles.py).

Calling Methods#

The method interface is similar to the interface of variables. In the example the special node client.nodes.objects, which is in fact just a shortcut to the 0:Objects node, is used to call the method. The call_method() must be called on the parent node of the actual method node.

Where to go next#

Subscriptions and reconnecting are in 03_read_write_subscribe.py; all examples are on the Examples page, and Client Overview covers the client in depth.