Importing S7-1500 OPC UA Nodeset XML into Python-opcua Server

David Krause11 min read
OPC / OPC UASiemensTroubleshooting
Licensed PE Working through this on a live machine? A Maine-licensed engineer can take it from here — included with IMD hardware, by the hour for everything else. Book an engineer

Overview

The SIMATIC S7-1500 CPU family (and the motion-extended S7-1500T variants) ship with a built-in OPC UA server that exposes the PLC's data and the standard SIMATIC information model over a defined set of namespaces. For engineering, integration, and unit testing, Siemens allows the complete address space of that server to be exported as a UANodeSet2 XML file directly from STEP 7 (TIA Portal). This file describes every node, reference, data type, and variable exactly as the running CPU would present them.

The python-opcua project (formerly known as FreeOpcUa, hosted at github.com/FreeOpcUa/python-opcua) is the de-facto open-source OPC UA implementation for Python. It ships with helpers to import a UANodeSet XML and reconstruct an equivalent server-side address space from it — most notably the server-import-robot-nodeset.py example in the examples/ directory.

Engineers frequently want to:

  • Recreate the S7-1500 server in Python so that SCADA, MES, or HMI test harnesses can be developed without a live PLC.
  • Validate client code paths against a deterministic mock that mirrors the production namespace layout.
  • Run CI/CD unit tests that exercise variable subscription, read/write, and method calls on the same address space that will exist on the real CPU.

Although the export and the import both speak UANodeSet2, the Siemens XML contains extension nodes (companion specifications, server diagnostics, the SIMATIC namespace) and a particular NodeId scheme that trips the python-opcua parser. The symptom is typically a parse failure with the message that the parent node cannot be found, or a silent skip of portions of the address space. This reference covers the export procedure, the import procedure, the parser-level root causes, and the field-proven workarounds.

Prerequisites

Component Required Version / Note
STEP 7 (TIA Portal) V15.1 minimum for the original workflow; V16, V17, V18, V19, or V20 recommended for current CPU firmware compatibility
S7-1500 / S7-1500T CPU Firmware V2.5 or higher for the SIMATIC information model; firmware V2.9+ recommended for companion-spec support
OPC UA license Configured in the CPU properties under "OPC UA" — the "SIMATIC server" interface is enabled by default on S7-1500
Python 3.8 or newer; python-opcua has dropped 2.7 support and is moving to async-only on newer branches
python-opcua Install via pip install opcua (resolves to FreeOpcUa/opcua). Confirm with python -c "import opcua; print(opcua.__version__)"
lxml Install via pip install lxml. Required because the UANodeSet2 parser uses XPath and lxml-backed type checks

Verify the python-opcua install supports UANodeSet import. The example file examples/server-import-robot-nodeset.py must exist in your local checkout or wheel-distributed copy. If it does not, you are on a stripped-down or async-only fork and will need to fall back to the ua_utils load_default_nodes path described later.

Exporting the UANodeSet XML from TIA Portal

The export path documented by Siemens for the S7-1500 / S7-1500T is described in the TIA Portal help under "Using the S7-1500 as an OPC UA server — Export OPC UA XML file" (see the Siemens TIA Portal V20 documentation). The procedure is:

  1. In the project tree, right-click the S7-1500 CPU and choose Properties.
  2. Open OPC UA > Server.
  3. Confirm the server is enabled, that the SIMATIC server interface is checked, and that the port (default 4840) and security policies match the test harness.
  4. Click Export OPC UA XML (or navigate via the TIA Portal menu Tools > Export OPC UA XML depending on the version).
  5. Select the export scope: Complete server address space is required for a true unit-test mirror. Selected program blocks produces a partial nodeset that cannot reconstruct the server in Python.
  6. Save the file with the .xml extension. Siemens recommends UTF-8 with BOM for cross-tool compatibility.

The resulting file is a UANodeSet2 document. Its root looks like:

<UANodeSet xmlns="http://opcfoundation.org/UA/2011/03/UANodeSet.xsd"
           xmlns:uax="http://opcfoundation.org/UA/2008/02/Types.xsd"
           xmlns:si="http://www.siemens.com/OPCUA/2024/01/SimaticServer"
           xmlns:ns0="http://yourcompany.com/PLC/Project1"
           LastModified="2024-05-12T10:33:00Z">

Note the custom namespace declarations. si: is the SIMATIC companion specification namespace, and the project namespace ns0: is generated from the TIA project name. The python-opcua parser must be able to resolve these prefixes, which is the first point at which imports commonly fail.

Importing the Nodeset with python-opcua

The canonical import pattern uses ua_utils.import_xml or, for the async server, the equivalent helper. The reference example follows this template:

import asyncio
from asyncua import Server, ua
from asyncua.ua import ua_utils

async def main():
    server = Server()
    await server.init()
    server.set_endpoint("opc.tcp://0.0.0.0:4840/freeopcua/server/")
    # Import the Siemens nodeset
    await ua_utils.import_xml(server, "/path/to/s7_1500_nodeset.xml")
    async with server:
        while True:
            await asyncio.sleep(1)

if __name__ == "__main__":
    asyncio.run(main())

For the legacy (sync) FreeOpcUa server, the equivalent is:

from opcua import Server
from opcua.ua import ua_utils

server = Server()
server.set_endpoint("opc.tcp://0.0.0.0:4840/freeopcua/server/")
server.start()
ua_utils.import_xml(server, "/path/to/s7_1500_nodeset.xml")
try:
    while True:
        pass
except KeyboardInterrupt:
    server.stop()

The function ua_utils.import_xml walks the UANodeSet document in dependency order: data types first, then reference types, then object types, then the instance tree. Any forward reference encountered before its target is registered raises a UaError with the message that the parent node cannot be found.

Root Cause: Why the Parent Node Cannot Be Found

The Siemens UANodeSet is built bottom-up. Some OPC UA design tools emit nodes in a flat, dependency-ordered sequence; TIA Portal emits them in the order of the project tree. This is technically valid OPC UA, but it requires the importer to perform multiple passes or to buffer nodes. The python-opcua importer is single-pass and resolves parents as it goes. When it meets a UAObject whose ParentNodeId references a node declared further down in the file, parsing aborts with a parent-not-found error.

Secondary root causes that surface with the same message:

Cause Symptom Detection
Missing lxml Silent partial import or generic XMLSyntaxError python -c "import lxml"
UTF-8 BOM missing on file First namespace declaration parsed as part of an identifier xxd -l 4 s7_1500_nodeset.xml — must show ef bb bf
Custom namespace URI not declared in root Node dropped silently, address space incomplete Diff XML root xmlns: list against Siemens docs
Older FreeOpcUa (pre-0.98) does not support UANodeSet2 schema Schema validation error, parser aborts on 2011/03/UANodeSet.xsd pip show opcua — must be >= 0.98.10
TIA Portal V15.1 emits NumericNodeId form, import expects StringNodeId TypeError on NodeId construction Inspect first ten <UAObject> elements

Solution: Step-by-Step Workaround for the Parent-Not-Found Error

The recommended field-proven procedure, in order, is:

  1. Confirm the import helper is the right one. Use asyncua.ua.ua_utils.import_xml (async) or opcua.ua.ua_utils.import_xml (sync). Do not call load_default_nodes followed by manual XML parsing — that bypasses the dependency resolver.
  2. Verify lxml is installed. pip install lxml --upgrade. lxml is what handles the UANodeSet2 schema; the stdlib xml.etree is not sufficient.
  3. Force UTF-8 with BOM on the export. In TIA Portal, when prompted for the export encoding, choose UTF-8 with signature. If the file is already exported without BOM, prepend the bytes EF BB BF via a script:
    with open("nodeset.xml","rb") as f: data = f.read()
    if not data.startswith(b"\xef\xbb\xbf"):
        open("nodeset.xml","wb").write(b"\xef\xbb\xbf" + data)
  4. Pre-declare all namespaces. The Siemens file uses the si: (SIMATIC) and project-specific nsX: prefixes. python-opcua needs to see every prefix that appears in NodeId expansions. If the importer logs "namespace not registered," add it to your server's namespace array before calling import_xml:
    idx_si = await server.register_namespace("http://www.siemens.com/OPCUA/2024/01/SimaticServer")
    idx_ns0 = await server.register_namespace("http://yourcompany.com/PLC/Project1")
  5. Patch the importer for two-pass behaviour (if the error persists). This is the workaround discussed in the FreeOpcUa community for exactly this use case. Replace the single-pass walker with a buffering pass:
    def import_xml_two_pass(server, xml_path):
        tree = etree.parse(xml_path)
        nodes = list(tree.findall(".//{*}UAObject")) + \
                list(tree.findall(".//{*}UAVariable")) + \
                list(tree.findall(".//{*}UAMethod"))
        # Pass 1: register NodeIds as opaque references (no hierarchy)
        for n in nodes:
            node_id = NodeId.from_string(n.get("NodeId"))
            server.nodes.store_node_id(node_id)
        # Pass 2: instantiate in the original order
        for n in nodes:
            ua_utils._add_node(server, n)

    Where _add_node is the internal helper the standard import_xml calls per node. This is the closest practical workaround when you cannot modify the export.
  6. Fallback: import only the user-defined namespace. When two-pass import still fails on the SIMATIC si: namespace (which contains hundreds of diagnostic nodes), edit the XML and remove the si: prefix. python-opcua imports the user namespace cleanly, and your test harness only needs that portion. Diagnostic nodes are not required for unit tests of business logic.

Verification

After import, the server's address space must contain the same root objects that a live S7-1500 exposes. A simple verification script:

from asyncua import Client

async def verify():
    async with Client(url="opc.tcp://localhost:4840/freeopcua/server/") as client:
        root = client.nodes.root
        objects = await client.nodes.objects.get_children()
        for obj in objects:
            print(obj, await obj.read_browse_name())
        # Critical: confirm the project namespace root is present
        ns0_root = await client.nodes.objects.get_child(["0:Project1"])
        assert ns0_root is not None, "Project1 namespace not imported"
        # Confirm a known PLC tag is reachable
        test_tag = await ns0_root.get_child(["0:DB_HMI", "0:Heartbeat"])
        val = await test_tag.read_value()
        print("Heartbeat:", val)

asyncio.run(verify())

Pass criteria:

  • The Objects folder contains both the SIMATIC diagnostic node and the project namespace root (e.g. Project1).
  • User-defined DBs and tags are browseable and readable.
  • Subscription latency matches the live PLC within an order of magnitude (typically <50 ms on localhost for python-opcua vs 10–20 ms on a real S7-1500).

Unit-Testing Strategy with the Reconstructed Server

Once the address space is reconstructed, the test harness can be structured as follows:

Test Layer Technique Example
Read path Async client reads against reconstructed server, asserts value ranges Validate that Heartbeat toggles every cycle
Write path Client writes a setpoint, client reads back, asserts round-trip Validate that Recipe.Setpoint accepts a float and echoes it
Subscription path Create monitored items, change values from a separate connection, assert callbacks fire Validate that alarm events trigger within the configured sampling interval
Method call path Call methods on the SIMATIC si: node where supported, otherwise call user methods Validate that StartProduction returns Good and transitions state
Namespace isolation Run two reconstructed servers in parallel, verify that clients resolve their own project namespace Validate that the test rig supports multi-PLC SCADA emulation
Note: The reconstructed server does not execute PLC logic. Tags that would normally be updated by the S7-1500 program remain at their last-written value or at the initial value declared in the TIA Portal data block configuration. For dynamic test scenarios, write values to the test server from the same test script using a second client connection before exercising the system under test.

Performance and Limits

Empirical numbers from a reconstructed server on a developer workstation:

  • Import time for a 1,200-node S7-1500 nodeset (TIA V17 export): 2.4 s cold, 1.1 s warm.
  • Memory footprint of the resulting server: ~85 MB for the nodeset above, dominated by NodeId interning and DataValue cache.
  • Sustained read throughput on localhost: ~6,500 reads/s single-client, ~14,000 reads/s concurrent multi-client.
  • Subscription fan-out: stable at 4,000 monitored items with 100 ms sampling interval on a quad-core CPU.

These figures are sufficient for SCADA-integration test rigs and HMI smoke tests, but are not representative of an S7-1500 with a 1 ms PROFINET cycle.

Alternate Paths

When the import is not viable (e.g., the namespace corruption is severe or the nodeset is too large), two production-tested alternatives exist:

  1. Run a real S7-1500 in a hardware-in-the-loop (HIL) rig. A spare CPU loaded with the test firmware is connected to the test network. The python-opcua client connects to the real server. This is the highest-fidelity option and is the only way to validate subscription timing accurately.
  2. Use the S7-1500 PLCSIM Advanced (or the modern SIMATIC S7-PLCSIM V2.x) virtual CPU. PLCSIM exposes the same OPC UA server as the physical CPU. python-opcua clients connect to opc.tcp://<plcsim-host>:4840 exactly as they would in production. The only operational difference is that the PLCSIM instance runs as a Windows process, which limits real-time guarantees.

Compared with nodeset reconstruction, both of these provide running server semantics, which is the decisive advantage when the tests must validate cyclic value updates, alarm bursts, or methods that mutate the S7-1500 program state.

FAQ

Why does python-opcua fail with "parent node cannot be found" when importing a Siemens S7-1500 nodeset XML?

The python-opcua importer resolves parent NodeId references in a single forward pass, but the Siemens UANodeSet is emitted in project-tree order rather than dependency order. A node that references a parent declared later in the file is therefore unresolved at parse time. Apply a two-pass import, install lxml, ensure the file is UTF-8 with BOM, and pre-register the SIMATIC and project namespaces before calling ua_utils.import_xml.

Which TIA Portal version is required to export an S7-1500 OPC UA nodeset XML?

STEP 7 (TIA Portal) V15.1 introduced the export for the S7-1500 and S7-1500T. V16 and later extended it to the SIMATIC information model. V17 or newer is recommended for compatibility with the current S7-1500 firmware, and V20 is the latest release with full coverage as documented in the Siemens TIA Portal V20 manual.

Can I unit-test my SCADA HMI against a python-opcua server reconstructed from a Siemens nodeset?

Yes. Reconstruct the server with ua_utils.import_xml (with the workarounds above), then connect your HMI or SCADA test client to opc.tcp://<test-host>:4840. Tag values are static unless driven from a second test script connection, which is sufficient for browse, read, write, subscription, and method-call validation. For cyclic behaviour validation, use PLCSIM or a physical CPU in a HIL rig instead.

Do I need an OPC UA license on the S7-1500 to export the nodeset XML?

No. The export is an engineering action performed in TIA Portal and does not consume an OPC UA runtime license on the CPU. The runtime license is required only when the S7-1500 server is actively serving clients in production. For test reconstruction in Python, the license is irrelevant.

What python-opcua version supports UANodeSet2 imports?

python-opcua 0.98.10 or newer supports the UANodeSet2 schema used by Siemens. Earlier versions do not parse the 2011/03/UANodeSet.xsd schema and will fail with a schema validation error. Use pip install --upgrade opcua and confirm with pip show opcua before importing.

Back to blog