Machine DataPythonPython 3.8+ · asyncuaMIT licenseIntermediate
Browse an OPC UA server and read a machine's variables (Python)
Most OPC UA examples hardcode a NodeId like ns=2;i=1005 — which is exactly the number that's different on every PLC and every machine builder's server. This script does the more useful thing: it browses the standard Objects folder for an object matching a name you give it, then reads back every variable under that object, whatever they're called. Tested end to end against a local OPC UA test server before this ever needed real hardware.
Before you run it
pip install asyncua- An OPC UA server endpoint URL and the browse name of the object it exposes for the machine
The code
"""Browse an OPC UA server for a named object and print its variable values.
Usage: python opcua_reader.py opc.tcp://192.168.0.20:4840/machine/ CNC-01
"""
import asyncio
import sys
from asyncua import Client
async def read_machine(url, object_name):
async with Client(url=url) as client:
objects = client.get_objects_node()
children = await objects.get_children()
target = None
for child in children:
name = (await child.read_browse_name()).Name
if name == object_name:
target = child
break
if target is None:
names = [(await c.read_browse_name()).Name for c in children]
sys.exit(f"'{object_name}' not found under Objects. Available: {names}")
for var in await target.get_children():
name = (await var.read_browse_name()).Name
try:
value = await var.read_value()
except Exception as e:
value = f"<not readable: {e}>"
print(f"{name}: {value}")
def main():
if len(sys.argv) < 3:
sys.exit("Usage: python opcua_reader.py <endpoint-url> <object-name>")
asyncio.run(read_machine(sys.argv[1], sys.argv[2]))
if __name__ == "__main__":
main()What you get
$ python opcua_reader.py opc.tcp://192.168.0.20:4840/machine/ CNC-01
ActualFeed: 320.5
SpindleSpeed: 4200
ExecutionStatus: ACTIVE
PartCount: 742
$ python opcua_reader.py opc.tcp://192.168.0.20:4840/machine/ CNC-99
'CNC-99' not found under Objects. Available: ['Locations', 'Server', 'Aliases', 'CNC-01']
How it works
client.get_objects_node()starts at OPC UA's standardObjectsfolder — every compliant server has one, which is what makes browsing-by-name portable across completely different PLCs and machine builders.- Matching on
read_browse_name().Nameinstead of a NodeId is the whole point: NodeIds are server-specific and often numeric; browse names are the human-readable labels an integrator actually chose. - The second
get_children()call — on the matched object, not onObjects— walks that machine's own variables. Nesting is exactly this simple in a flat OPC UA model; deeper hierarchies just repeat the pattern one level further down. - Failed reads are caught per-variable, not for the whole script — one write-only or momentarily bad node shouldn't stop you from seeing every other value.
Gotchas & honest limits
- Server security matters more here than in FOCAS or MTConnect — this script uses
Client(url=...)with no security policy, which many production OPC UA servers reject outright. Real machines usually need a certificate and a signed/encrypted policy; check your PLC vendor's OPC UA setup guide. - Browse names aren't guaranteed unique across a whole server, only within their parent — matching under
Objectsdirectly is safe; searching deeper trees needs a path-aware browse, not this flat loop. - Value types come back as native Python types via
asyncua's automatic conversion — a UInt16 and a Double both just print as numbers, so if you need to distinguish them, read theVariantTypeinstead of the bare value. - This is a read-only browser. Writing values (
var.write_value(...)) uses the same node objects but is a bigger safety conversation — don't wire it to anything until you've read your server's write-access model.
Goes deeper
Want this adapted to your shop — or built into a real tool?
Samples are the free 80%. The last 20% is the part I do for a living.