Reading & Writing¶
You're building a small control loop against the distilling example server: discover the writables, peek at the current state, then change it. The first half of that loop — see what the still is doing and tell it to do something different — lives on this page. The next page, Call a method, flips the on-switch; the one after that, Monitor data changes, is where you stop polling and let the server push values to you.
Once you know which NodeIds a server exposes — from browsing — the next step is reading their current values and, where the server allows it, writing new ones. Reads accept one node or a list; writes also accept a mapping. Both can target any attribute. Value-only reads raise on failure; raw reads retain per-target statuses in DataValues, and writes return statuses.
This page walks through read and write primitives:
- Read one or several variables.
- Write one or several variables.
- Read and write non-
Valueattributes.
Info
This tutorial expects the example server running in the background, and assumes you know how to create and connect a client and how to browse the address space. The NodeIds used below are the ones exposed by the distilling example server's DistillingSystem object at ns=1;i=1000.
Read one or several variables¶
client.read(target) is the basic read service call: hand it a NodeId and get back the current value of its Value attribute. The simplest read takes one NodeId and returns its current value.
Let's look into the Kettle at ns=1;i=1300 — its Level, Temperature and WashStart:
Level = client.read("ns=1;i=1301")
Temperature = client.read("ns=1;i=1302")
WashStart = client.read("ns=1;i=1303")
To read several variables in one round-trip, pass a list of NodeIds. The result is a list in the same order as the input:
The values of the distillig server will be different on every read — Level climbs as the kettle fills, Temperature rises during heating, and WashStart records the kettle level at the moment the still started producing spirit.
If a node does not exist or the server rejects a value-only read, scalar and
list forms both raise o6.StatusCodeError. Its .code preserves the OPC UA
status. A list failure includes the failed index in an exception note.
from o6 import Client, StatusCodeError
with Client("opc.tcp://localhost:4840") as client:
try:
values = client.read(["ns=1;i=1301", "ns=1;i=1302"])
except StatusCodeError as e:
print("read failed:", e.code, getattr(e, "__notes__", []))
Use valueOnly=False to receive a DataValue for every target, including failed
ones. timestampsToReturn selects SOURCE (the default), SERVER, BOTH, or
NEITHER from ns0.datatypes.TimestampsToReturn. Service failures still raise
StatusCodeError. The same read options and result shapes work with
server.read; local access uses administrative privileges.
Tip
Any string form accepted by o6.NodeId(...) works here — i=, s=, ns=...;i=..., nsu=...;i=..., even the shortname URI form once the matching nodeset is loaded. See NodeIds and namespace info for the full syntax.
Putting it all together¶
import o6
from o6 import Client
with Client("opc.tcp://localhost:4840") as client:
# One read per round-trip
level = client.read("ns=1;i=1301")
temperature = client.read("ns=1;i=1302")
# Or batch them in a single round-trip
kettle = client.read([
"ns=1;i=1301",
"ns=1;i=1302",
"ns=1;i=1303",
])
print(kettle)
print("level value:", level, " is o6.Float?", isinstance(level, o6.Float))
Write one or several variables¶
client.write(target, value) is the basic write service call: hand it a NodeId and a new value, and the server replaces the Value attribute. In the distilling example the only writable variables on the server are Status.Operating (ns=1;i=1203) and Status.Setpoint (ns=1;i=1204) — everything under Kettle, Distillate, Actuators and Events is read-only. You can see that directly from AccessLevel:
import o6
from o6 import AttributeId, Client
from o6.ns.ns0.datatypes import NodeClass
with Client("opc.tcp://localhost:4840") as client:
writable = client.browse(
"ns=1;i=1000",
nodeClassMask=NodeClass.VARIABLE,
)
for ref in writable:
access = client.read(ref.nodeId, attr=AttributeId.ACCESS_LEVEL)
# bit 1 of AccessLevel = "writable"
if access & 2:
print(ref.browseName.name, "→", o6.NodeId(f"nsu={ref.nodeId.ns.uri};{ref.nodeId.id}"))
To pause the still, write False to Operating:
To write several variables in one round-trip, pass a list of NodeIds and a parallel list of values (same order, same length). The result is a list of StatusCodes, one per write:
A StatusCode other than Good on any entry means that particular write failed — the other writes in the same call still happened, and client.write does not raise.
Info
If you'd rather treat writes as key/value pairs, client.write({"ns=1;i=1204": 90.0}) is accepted as well — useful when the targets are computed dynamically and live in a dict.
Putting it all together¶
from o6 import Client
with Client("opc.tcp://localhost:4840") as client:
# Single write
client.write("ns=1;i=1203", False)
# Or batch them in a single round-trip
statuses = client.write(
["ns=1;i=1203", "ns=1;i=1204"],
[True, 90.0],
)
print(statuses)
Read and write non-Value attributes¶
By default both read and write operate on the Value attribute of a variable. The attr= keyword lets you target a different attribute — BrowseName, DisplayName, NodeClass, Description, DataType, ValueRank, AccessLevel, and so on.
Read a non-Value attribute¶
Let's inspect the Setpoint variable's metadata without reading its value:
name = client.read("ns=1;i=1204", attr="BrowseName")
display = client.read("ns=1;i=1204", attr="DisplayName")
klass = client.read("ns=1;i=1204", attr=AttributeId.NODE_CLASS)
access = client.read("ns=1;i=1204", attr=AttributeId.ACCESS_LEVEL)
# `name` is a QualifiedName; `.name` gives just the bare browse name.
print(name.name) # "Setpoint"
print(display.text) # LocalizedText.text — the bare string
print(int(klass)) # raw integer (the wire value, 2 == VARIABLE)
print(access) # raw integer
The attr= value can be either a string (case-insensitive, ignores punctuation — "BrowseName", "browse_name", "BROWSENAME" all match) or an o6.AttributeId enum member if you want exactness.
Write a non-Value attribute¶
Writing a non-Value attribute uses both value= and attr=. Attributes that are themselves structured types — Description is a LocalizedText — must be wrapped in the matching type:
from o6 import Client, LocalizedText, QualifiedName, StatusCodeError
with Client("opc.tcp://localhost:4840") as client:
try:
status = client.write(
"ns=1;i=1204",
value=LocalizedText("Target kettle temperature"),
attr="Description",
)
except StatusCodeError as e:
# The distillery server denies writes to non-Value attributes
# (BAD_USER_ACCESS_DENIED); other servers may allow them.
# The call shape is correct either way — it's the server's
# AccessControl that decides.
status = e.code
print("description write:", status)
try:
client.write(
"ns=1;i=1204",
value=QualifiedName("Setpoint"),
attr="BrowseName",
)
except StatusCodeError as e:
# Same caveat: server-side access control may refuse.
print("browsename write refused:", e.code)
Putting it all together¶
import o6
from o6 import AttributeId, Client
with Client("opc.tcp://localhost:4840") as client:
# Read a structured attribute (QualifiedName)
name = client.read("ns=1;i=1204", attr="BrowseName")
print(name.name) # "Setpoint" — QualifiedName.name strips the namespace
# Read a scalar attribute (NodeClass enum -> raw int on the low-level read path)
klass = client.read("ns=1;i=1204", attr=AttributeId.NODE_CLASS)
print(int(klass)) # 2 — VARIABLE
# AccessLevel: bit 1 set means writable
access = client.read("ns=1;i=1204", attr=AttributeId.ACCESS_LEVEL)
print("writable:", bool(access & 2))
# The distillery server only allows `Value` writes; non-Value
# attribute writes return BAD_USER_ACCESS_DENIED. The call
# shape is correct — switch to a Value write (no `attr=`) for
# an example that actually succeeds end-to-end:
status = client.write("ns=1;i=1204", 90.0) # writes the Value attribute
print("setpoint write:", status) # GOOD
What's next?¶
- Call a method — same shape of call, but for methods on objects.
- Low-level service calls — drop down to the raw
Read/Writeservice when you need full control over the request.