FreeOpcUa / opcua-asyncio

OPC UA library for python >= 3.7
GNU Lesser General Public License v3.0
1.13k stars 362 forks source link

get_path created BrowsePath finds not match via tranlate_browsepaths #1706

Open Fliens opened 1 month ago

Fliens commented 1 month ago

This is a BUG REPORT for issues in the existing code.

Describe the bug
I'm using the get_path(as_string=True) function from the node class. The function chooses a 'wrong' path so that the translate_browsepaths() function generates the status code "BadNoMatch"

The Prosys Browser can also export a browse path (right click on node) which does find a match via the translate_browsepaths() function.

The node 'exists' in both paths but only the path from prosys works.

To Reproduce
Steps to reproduce the behavior incl code:

import asyncio

from asyncua import Client, ua
from asyncua.ua.status_codes import get_name_and_doc

async def main():
    url = "opc.tcp://uademo.prosysopc.com:53530/OPCUA/SimulationServer/"

    async with Client(url=url) as client:
        # The node these paths should lead to:
        # ns=4;s=1001/0:Direction
        # Working path: /Objects/3:Simulation/3:Counter/2:Signal/4:Direction

        results: ua.BrowsePathResult = await client.translate_browsepaths(
            starting_node=client.nodes.root.nodeid,
            relative_paths=[
                "/Objects/3:Simulation/3:Counter/2:Signal/4:Direction",
                "/0:Objects/0:Server/2:ValueSimulations/2:Signal/4:Direction",
            ],
        )

        for x in results:
            print(get_name_and_doc(x.StatusCode.value), x)

        # Output
        # ('Good', 'The operation succeeded.') BrowsePathResult(StatusCode_=StatusCode(value=0), Targets=                [BrowsePathTarget(TargetId=ExpandedNodeId(Identifier='1001/0:Direction', NamespaceIndex=4, NodeIdType=        <NodeIdType.String: 3>, NamespaceUri=None, ServerIndex=0), RemainingPathIndex=4294967295)])
        # ('BadNoMatch', 'The requested operation has no match to return.')         BrowsePathResult(StatusCode_=StatusCode(value=2154758144), Targets=[])

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

The browse path that was exported from Prosys works: /Objects/3:Simulation/3:Counter/2:Signal/4:Direction

Expected behavior
The function get_path(as_string=True) should generate a path that works with the translate_browsepaths() function.

Screenshots

image

Version
Python-Version:3.11.5
opcua-asyncio Version (e.g. master branch, 0.9):

Fliens commented 1 month ago

A way to get all paths of a node would be nice too This way you could export the paths and later on reconstruct the address space 1:1

AndreasHeine commented 1 month ago

A way to get all paths of a node would be nice too This way you could export the paths and later on reconstruct the address space 1:1

as far as i know not part of OPC UA Spec.! For TranslateBrowsePaths the client needs to know the path to the node, from which the client wants the nodeid from!

Fliens commented 1 month ago

A way to get all paths of a node would be nice too This way you could export the paths and later on reconstruct the address space 1:1

as far as i know not part of OPC UA Spec.! For TranslateBrowsePaths the client needs to know the path to the node, from which the client wants the nodeid from!

Yeah that's what I want to do but the get_path function returns a path in the case above that can't be translated to the actual node.

From my understanding the get_path function should always return a valid path that can be translated but that does not seem to be the case.

AndreasHeine commented 1 month ago

the only difference is: "/Objects" "/0:Objects" right?

Fliens commented 1 month ago

the only difference is: "/Objects" "/0:Objects" right?

No this does not affect the translate_browsepaths() function

The node in question is referenced twice (as shown in the screenshot on the right side). Both paths are valid by eye (when following the nodes in the browser gui) but only the first path (exported from the prosys gui) works with the translate_browsepaths() function.

AndreasHeine commented 1 month ago

get_path just uses the first reference see: https://github.com/FreeOpcUa/opcua-asyncio/blob/1343fa761ce5b2ece79b64041463b86486b81534/asyncua/common/node.py#L538

AndreasHeine commented 1 month ago

browsename is correctly formatted: https://github.com/FreeOpcUa/opcua-asyncio/blob/1343fa761ce5b2ece79b64041463b86486b81534/asyncua/ua/uatypes.py#L679

and the relative path implementation looks right on the first look https://github.com/FreeOpcUa/opcua-asyncio/blob/master/asyncua/ua/relative_path.py https://reference.opcfoundation.org/Core/Part4/v105/docs/A.2#TableA.2

Fliens commented 1 month ago

Yeah I guess so but this seems counterintuitive since I would assume that I could directly put the path from the get_path function (transformed into a valid string) into the translate_path function and would get the same node back

AndreasHeine commented 1 month ago

but both paths are valid!

Fliens commented 1 month ago

but both paths are valid!

But the second one does not work. ('BadNoMatch', 'The requested operation has no match to return.')

You can run the code yourself and connect to the server yourself (via gui), the opcua server in the example is public.

I've extended the example code to include the just read path via get_path()

import asyncio

from asyncua import Client, Node, ua
from asyncua.ua.status_codes import get_name_and_doc

async def main():
    url = "opc.tcp://uademo.prosysopc.com:53530/OPCUA/SimulationServer/"

    async with Client(url=url) as client:
        # The node these paths should lead to:
        # ns=4;s=1001/0:Direction
        # Working path: /Objects/3:Simulation/3:Counter/2:Signal/4:Direction

        test_node: Node = client.get_node("ns=4;s=1001/0:Direction")
        test_path = "/".join(
            (await test_node.get_path(as_string=True))[1:]
        )  # 0:Objects/0:Server/2:ValueSimulations/2:Signal/4:Direction

        results: ua.BrowsePathResult = await client.translate_browsepaths(
            starting_node=client.nodes.root.nodeid,
            relative_paths=[
                "0:Objects/3:Simulation/3:Counter/2:Signal/4:Direction",
                "0:Objects/0:Server/2:ValueSimulations/2:Signal/4:Direction",
                test_path,  # same as the one above
            ],
        )

        for x in results:
            print(get_name_and_doc(x.StatusCode.value), x)

        # Result:
        # ('Good', 'The operation succeeded.') BrowsePathResult(StatusCode_=StatusCode(value=0), Targets=[BrowsePathTarget(TargetId=ExpandedNodeId(Identifier='1001/0:Direction', NamespaceIndex=4, NodeIdType=<NodeIdType.String: 3>, NamespaceUri=None, ServerIndex=0), RemainingPathIndex=4294967295)])
        # ('BadNoMatch', 'The requested operation has no match to return.') BrowsePathResult(StatusCode_=StatusCode(value=2154758144), Targets=[])
        # ('BadNoMatch', 'The requested operation has no match to return.') BrowsePathResult(StatusCode_=StatusCode(value=2154758144), Targets=[])

if __name__ == "__main__":
    asyncio.run(main())
AndreasHeine commented 1 month ago

"BadNoMatch" is a response from Server! The Paths are correct and valid according to the OPC UA Spec.!

We just use the first hierarchical Reference and follow them up ("inverse" direction)... it does not matter which you chose because they point to the same NodeId/Node...

Fliens commented 1 month ago

Yes I understand that the error is a response from the server. And yeah I checked with the spec, the paths are valid.

I would prefer that the get_path would return all paths but that is my issue since that functionality is not a part of the spec.

But I still don't understand why the translate_browsepaths() function does not find a match.

The code in this comment creates a valid path that the translate service on the server somehow can't resolve.