Passed
Push — master ( dba4ef...8b73a4 )
by Olivier
02:57
created

asyncua.tools._uawrite()   B

Complexity

Conditions 2

Size

Total Lines 40
Code Lines 38

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 38
nop 0
dl 0
loc 40
rs 8.968
c 0
b 0
f 0
1
import asyncio
2
import logging
3
import sys
4
import argparse
5
from datetime import datetime, timedelta
6
import math
7
import time
8
import concurrent.futures._base
9
10
try:
11
    from IPython import embed
12
except ImportError:
13
    import code
14
15
    def embed():
16
        code.interact(local=dict(globals(), **locals()))
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable code does not seem to be defined.
Loading history...
17
18
from asyncua import ua
19
from asyncua import Client, Server
20
from asyncua import Node, uamethod
21
from asyncua import sync
22
from asyncua.ua.uaerrors import UaStatusCodeError
23
24
25
if sys.version_info.major < 3:
26
    raise ValueError("This is a python 3 application")
27
if sys.version_info.minor >= 7:
28
    def run(coro):
29
        return asyncio.run(coro)
30
else:
31
    def run(coro):
32
        loop = asyncio.new_event_loop()
33
        return loop.run_until_complete(coro)
34
35
36
def add_minimum_args(parser):
37
    parser.add_argument("-u",
38
                        "--url",
39
                        help="URL of OPC UA server (for example: opc.tcp://example.org:4840)",
40
                        default='opc.tcp://localhost:4840',
41
                        metavar="URL")
42
    parser.add_argument("-v",
43
                        "--verbose",
44
                        dest="loglevel",
45
                        choices=['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'],
46
                        default='WARNING',
47
                        help="Set log level")
48
    parser.add_argument("--timeout",
49
                        dest="timeout",
50
                        type=int,
51
                        default=1,
52
                        help="Set socket timeout (NOT the diverse UA timeouts)")
53
54
55
def add_common_args(parser, default_node='i=84', require_node=False):
56
    add_minimum_args(parser)
57
    parser.add_argument("-n",
58
                        "--nodeid",
59
                        help="Fully-qualified node ID (for example: i=85). Default: root node",
60
                        default=default_node,
61
                        required=require_node,
62
                        metavar="NODE")
63
    parser.add_argument("-p",
64
                        "--path",
65
                        help="Comma separated browse path to the node starting at NODE (for example: 3:Mybject,3:MyVariable)",
66
                        default='',
67
                        metavar="BROWSEPATH")
68
    parser.add_argument("-i",
69
                        "--namespace",
70
                        help="Default namespace",
71
                        type=int,
72
                        default=0,
73
                        metavar="NAMESPACE")
74
    parser.add_argument("--security",
75
                        help="Security settings, for example: Basic256Sha256,SignAndEncrypt,cert.der,pk.pem[,server_cert.der]. Default: None",
76
                        default='')
77
    parser.add_argument("--user",
78
                        help="User name for authentication. Overrides the user name given in the URL.")
79
    parser.add_argument("--password",
80
                        help="Password name for authentication. Overrides the password given in the URL.")
81
82
83
def _require_nodeid(parser, args):
84
    # check that a nodeid has been given explicitly, a bit hackish...
85
    if args.nodeid == "i=84" and args.path == "":
86
        parser.print_usage()
87
        print(f"{parser.prog}: error: A NodeId or BrowsePath is required")
88
        sys.exit(1)
89
90
91
def parse_args(parser, requirenodeid=False):
92
    args = parser.parse_args()
93
    # logging.basicConfig(format="%(levelname)s: %(message)s", level=getattr(logging, args.loglevel))
94
    logging.basicConfig(level=getattr(logging, args.loglevel))
95
    if args.url and '://' not in args.url:
96
        logging.info(f"Adding default scheme {ua.OPC_TCP_SCHEME} to URL {args.url}")
97
        args.url = ua.OPC_TCP_SCHEME + '://' + args.url
98
    if requirenodeid:
99
        _require_nodeid(parser, args)
100
    return args
101
102
103
async def get_node(client, args):
104
    node = client.get_node(args.nodeid)
105
    if args.path:
106
        path = args.path.split(",")
107
        if node.nodeid == ua.NodeId(84, 0) and path[0] == "0:Root":
108
            # let user specify root if not node given
109
            path = path[1:]
110
        node = await node.get_child(path)
111
    return node
112
113
114
def uaread():
115
    run(_uaread())
116
117
118
async def _uaread():
119
    parser = argparse.ArgumentParser(description="Read attribute of a node, per default reads value of a node")
120
    add_common_args(parser)
121
    parser.add_argument("-a",
122
                        "--attribute",
123
                        dest="attribute",
124
                        type=int,
125
                        default=ua.AttributeIds.Value,
126
                        help="Set attribute to read")
127
    parser.add_argument("-t",
128
                        "--datatype",
129
                        dest="datatype",
130
                        default="python",
131
                        choices=['python', 'variant', 'datavalue'],
132
                        help="Data type to return")
133
134
    args = parse_args(parser, requirenodeid=True)
135
136
    client = Client(args.url, timeout=args.timeout)
137
    await client.set_security_string(args.security)
138
    await client.connect()
139
140
    try:
141
        node = await get_node(client, args)
142
        attr = await node.read_attribute(args.attribute)
143
        if args.datatype == "python":
144
            print(attr.Value.Value)
145
        elif args.datatype == "variant":
146
            print(attr.Value)
147
        else:
148
            print(attr)
149
    except Exception as e:
150
        print(e)
151
        sys.exit(1)
152
    finally:
153
        await client.disconnect()
154
    sys.exit(0)
155
156
157
def _args_to_array(val, array):
158
    if array == "guess":
159
        if "," in val:
160
            array = "true"
161
    if array == "true":
162
        val = val.split(",")
163
    return val
164
165
166
def _arg_to_bool(val):
167
    return val in ("true", "True")
168
169
170
def _arg_to_variant(val, array, ptype, varianttype=None):
171
    val = _args_to_array(val, array)
172
    if isinstance(val, list):
173
        val = [ptype(i) for i in val]
174
    else:
175
        val = ptype(val)
176
    if varianttype:
177
        return ua.Variant(val, varianttype)
178
    else:
179
        return ua.Variant(val)
180
181
182
def _val_to_variant(val, args):
183
    array = args.array
184
    if args.datatype == "guess":
185
        if val in ("true", "True", "false", "False"):
186
            return _arg_to_variant(val, array, _arg_to_bool)
187
        try:
188
            return _arg_to_variant(val, array, int)
189
        except ValueError:
190
            try:
191
                return _arg_to_variant(val, array, float)
192
            except ValueError:
193
                return _arg_to_variant(val, array, str)
194
    elif args.datatype == "bool":
195
        if val in ("1", "True", "true"):
196
            return ua.Variant(True, ua.VariantType.Boolean)
197
        else:
198
            return ua.Variant(False, ua.VariantType.Boolean)
199
    elif args.datatype == "sbyte":
200
        return _arg_to_variant(val, array, int, ua.VariantType.SByte)
201
    elif args.datatype == "byte":
202
        return _arg_to_variant(val, array, int, ua.VariantType.Byte)
203
    # elif args.datatype == "uint8":
204
        # return _arg_to_variant(val, array, int, ua.VariantType.Byte)
205
    elif args.datatype == "uint16":
206
        return _arg_to_variant(val, array, int, ua.VariantType.UInt16)
207
    elif args.datatype == "uint32":
208
        return _arg_to_variant(val, array, int, ua.VariantType.UInt32)
209
    elif args.datatype == "uint64":
210
        return _arg_to_variant(val, array, int, ua.VariantType.UInt64)
211
    # elif args.datatype == "int8":
212
        # return ua.Variant(int(val), ua.VariantType.Int8)
213
    elif args.datatype == "int16":
214
        return _arg_to_variant(val, array, int, ua.VariantType.Int16)
215
    elif args.datatype == "int32":
216
        return _arg_to_variant(val, array, int, ua.VariantType.Int32)
217
    elif args.datatype == "int64":
218
        return _arg_to_variant(val, array, int, ua.VariantType.Int64)
219
    elif args.datatype == "float":
220
        return _arg_to_variant(val, array, float, ua.VariantType.Float)
221
    elif args.datatype == "double":
222
        return _arg_to_variant(val, array, float, ua.VariantType.Double)
223
    elif args.datatype == "string":
224
        return _arg_to_variant(val, array, str, ua.VariantType.String)
225
    elif args.datatype == "datetime":
226
        raise NotImplementedError
227
    elif args.datatype == "Guid":
228
        return _arg_to_variant(val, array, bytes, ua.VariantType.Guid)
229
    elif args.datatype == "ByteString":
230
        return _arg_to_variant(val, array, bytes, ua.VariantType.ByteString)
231
    elif args.datatype == "xml":
232
        return _arg_to_variant(val, array, str, ua.VariantType.XmlElement)
233
    elif args.datatype == "nodeid":
234
        return _arg_to_variant(val, array, ua.NodeId.from_string, ua.VariantType.NodeId)
235
    elif args.datatype == "expandednodeid":
236
        return _arg_to_variant(val, array, ua.ExpandedNodeId.from_string, ua.VariantType.ExpandedNodeId)
237
    elif args.datatype == "statuscode":
238
        return _arg_to_variant(val, array, int, ua.VariantType.StatusCode)
239
    elif args.datatype in ("qualifiedname", "browsename"):
240
        return _arg_to_variant(val, array, ua.QualifiedName.from_string, ua.VariantType.QualifiedName)
241
    elif args.datatype == "LocalizedText":
242
        return _arg_to_variant(val, array, ua.LocalizedText, ua.VariantType.LocalizedText)
243
244
245
async def _configure_client_with_args(client, args):
246
    if args.user:
247
        client.set_user(args.user)
248
    if args.password:
249
        client.set_password(args.password)
250
    await client.set_security_string(args.security)
251
252
253
def uawrite():
254
    run(_uawrite())
255
256
257
async def _uawrite():
258
    parser = argparse.ArgumentParser(description="Write attribute of a node, per default write value of node")
259
    add_common_args(parser)
260
    parser.add_argument("-a",
261
                        "--attribute",
262
                        dest="attribute",
263
                        type=int,
264
                        default=ua.AttributeIds.Value,
265
                        help="Set attribute to read")
266
    parser.add_argument("-l",
267
                        "--list",
268
                        "--array",
269
                        dest="array",
270
                        default="guess",
271
                        choices=["guess", "true", "false"],
272
                        help="Value is an array")
273
    parser.add_argument("-t",
274
                        "--datatype",
275
                        dest="datatype",
276
                        default="guess",
277
                        choices=["guess", 'byte', 'sbyte', 'nodeid', 'expandednodeid', 'qualifiedname', 'browsename', 'string', 'float', 'double', 'int16', 'int32', "int64", 'uint16', 'uint32', 'uint64', "bool", "string", 'datetime', 'bytestring', 'xmlelement', 'statuscode', 'localizedtext'],
278
                        help="Data type to return")
279
    parser.add_argument("value",
280
                        help="Value to be written",
281
                        metavar="VALUE")
282
    args = parse_args(parser, requirenodeid=True)
283
284
    client = Client(args.url, timeout=args.timeout)
285
    await _configure_client_with_args(client, args)
286
    try:
287
        await client.connect()
288
        node = await get_node(client, args)
289
        val = _val_to_variant(args.value, args)
290
        await node.write_attribute(args.attribute, ua.DataValue(val))
291
    except Exception as e:
292
        print(e)
293
        sys.exit(1)
294
    finally:
295
        await client.disconnect()
296
    sys.exit(0)
297
298
def uals():
299
    run(_uals())
300
301
302
async def _uals():
303
    parser = argparse.ArgumentParser(description="Browse OPC-UA node and print result")
304
    add_common_args(parser)
305
    parser.add_argument("-l",
306
                        dest="long_format",
307
                        const=3,
308
                        nargs="?",
309
                        type=int,
310
                        help="use a long listing format")
311
    parser.add_argument("-d",
312
                        "--depth",
313
                        default=1,
314
                        type=int,
315
                        help="Browse depth")
316
317
    args = parse_args(parser)
318
    if args.long_format is None:
319
        args.long_format = 1
320
321
    client = Client(args.url, timeout=args.timeout)
322
    await _configure_client_with_args(client, args)
323
    try:
324
        async with client:
325
            node = await get_node(client, args)
326
            print(f"Browsing node {node} at {args.url}\n")
327
            if args.long_format == 0:
328
                await _lsprint_0(node, args.depth - 1)
329
            elif args.long_format == 1:
330
                await _lsprint_1(node, args.depth - 1)
331
            else:
332
                _lsprint_long(node, args.depth - 1)
333
    except (OSError, concurrent.futures._base.TimeoutError) as e:
334
        print(e)
335
        sys.exit(1)
336
    sys.exit(0)
337
338
339
async def _lsprint_0(node, depth, indent=""):
340
    if not indent:
341
        print("{0:30} {1:25}".format("DisplayName", "NodeId"))
342
        print("")
343
    for desc in await node.get_children_descriptions():
344
        print("{0}{1:30} {2:25}".format(indent, desc.DisplayName.to_string(), desc.NodeId.to_string()))
345
        if depth:
346
            await _lsprint_0(Node(node.server, desc.NodeId), depth - 1, indent + "  ")
347
348
349
async def _lsprint_1(node, depth, indent=""):
350
    if not indent:
351
        print("{0:30} {1:25} {2:25} {3:25}".format("DisplayName", "NodeId", "BrowseName", "Value"))
352
        print("")
353
354
    for desc in await node.get_children_descriptions():
355
        if desc.NodeClass == ua.NodeClass.Variable:
356
            try:
357
                val = await Node(node.server, desc.NodeId).read_value()
358
            except UaStatusCodeError as err:
359
                val = "Bad (0x{0:x})".format(err.code)
360
            print("{0}{1:30} {2!s:25} {3!s:25}, {4!s:3}".format(indent, desc.DisplayName.to_string(), desc.NodeId.to_string(), desc.BrowseName.to_string(), val))
361
        else:
362
            print("{0}{1:30} {2!s:25} {3!s:25}".format(indent, desc.DisplayName.to_string(), desc.NodeId.to_string(), desc.BrowseName.to_string()))
363
        if depth:
364
            await _lsprint_1(Node(node.server, desc.NodeId), depth - 1, indent + "  ")
365
366
367
def _lsprint_long(pnode, depth, indent=""):
368
    if not indent:
369
        print("{0:30} {1:25} {2:25} {3:10} {4:30} {5:25}".format("DisplayName", "NodeId", "BrowseName", "DataType", "Timestamp", "Value"))
370
        print("")
371
    for node in pnode.get_children():
372
        attrs = node.read_attributes([ua.AttributeIds.DisplayName,
373
                                     ua.AttributeIds.BrowseName,
374
                                     ua.AttributeIds.NodeClass,
375
                                     ua.AttributeIds.WriteMask,
376
                                     ua.AttributeIds.UserWriteMask,
377
                                     ua.AttributeIds.DataType,
378
                                     ua.AttributeIds.Value])
379
        name, bname, nclass, mask, umask, dtype, val = [attr.Value.Value for attr in attrs]
380
        update = attrs[-1].ServerTimestamp
381
        if nclass == ua.NodeClass.Variable:
382
            print("{0}{1:30} {2:25} {3:25} {4:10} {5!s:30} {6!s:25}".format(indent, name.to_string(), node.nodeid.to_string(), bname.to_string(), dtype.to_string(), update, val))
383
        else:
384
            print("{0}{1:30} {2:25} {3:25}".format(indent, name.to_string(), bname.to_string(), node.nodeid.to_string()))
385
        if depth:
386
            _lsprint_long(node, depth - 1, indent + "  ")
387
388
389
class SubHandler(object):
390
391
    def datachange_notification(self, node, val, data):
392
        print("New data change event", node, val, data)
393
394
    def event_notification(self, event):
395
        print("New event", event)
396
397
398
def uasubscribe():
399
    run(_uasubscribe())
400
401
402
async def _uasubscribe():
403
    parser = argparse.ArgumentParser(description="Subscribe to a node and print results")
404
    add_common_args(parser)
405
    parser.add_argument("-t",
406
                        "--eventtype",
407
                        dest="eventtype",
408
                        default="datachange",
409
                        choices=['datachange', 'event'],
410
                        help="Event type to subscribe to")
411
412
    args = parse_args(parser, requirenodeid=False)
413
    if args.eventtype == "datachange":
414
        _require_nodeid(parser, args)
415
    else:
416
        # FIXME: this is broken, someone may have written i=84 on purpose
417
        if args.nodeid == "i=84" and args.path == "":
418
            args.nodeid = "i=2253"
419
420
    client = Client(args.url, timeout=args.timeout)
421
    await _configure_client_with_args(client, args)
422
    await client.connect()
423
    try:
424
        node = await get_node(client, args)
425
        handler = SubHandler()
426
        sub = await client.create_subscription(500, handler)
427
        if args.eventtype == "datachange":
428
            await sub.subscribe_data_change(node)
429
        else:
430
            await sub.subscribe_events(node)
431
        print("Type Ctr-C to exit")
432
        while True:
433
            time.sleep(1)
434
    finally:
435
        await client.disconnect()
436
437
438
def application_to_strings(app):
439
    result = [('Application URI', app.ApplicationUri)]
440
    optionals = [
441
        ('Product URI', app.ProductUri),
442
        ('Application Name', app.ApplicationName.to_string()),
443
        ('Application Type', str(app.ApplicationType)),
444
        ('Gateway Server URI', app.GatewayServerUri),
445
        ('Discovery Profile URI', app.DiscoveryProfileUri),
446
    ]
447
    for (n, v) in optionals:
448
        if v:
449
            result.append((n, v))
450
    for url in app.DiscoveryUrls:
451
        result.append(('Discovery URL', url))
452
    return result  # ['{}: {}'.format(n, v) for (n, v) in result]
453
454
455
def cert_to_string(der):
456
    if not der:
457
        return '[no certificate]'
458
    try:
459
        from .crypto import uacrypto
460
    except ImportError:
461
        return f"{len(der)} bytes"
462
    cert = uacrypto.x509_from_der(der)
463
    return uacrypto.x509_to_string(cert)
464
465
466
def endpoint_to_strings(ep):
467
    result = [('Endpoint URL', ep.EndpointUrl)]
468
    result += application_to_strings(ep.Server)
469
    result += [
470
        ('Server Certificate', cert_to_string(ep.ServerCertificate)),
471
        ('Security Mode', str(ep.SecurityMode)),
472
        ('Security Policy URI', ep.SecurityPolicyUri)]
473
    for tok in ep.UserIdentityTokens:
474
        result += [
475
            ('User policy', tok.PolicyId),
476
            ('  Token type', str(tok.TokenType))]
477
        if tok.IssuedTokenType or tok.IssuerEndpointUrl:
478
            result += [
479
                ('  Issued Token type', tok.IssuedTokenType),
480
                ('  Issuer Endpoint URL', tok.IssuerEndpointUrl)]
481
        if tok.SecurityPolicyUri:
482
            result.append(('  Security Policy URI', tok.SecurityPolicyUri))
483
    result += [
484
        ('Transport Profile URI', ep.TransportProfileUri),
485
        ('Security Level', ep.SecurityLevel)]
486
    return result
487
488
489
def uaclient():
490
    run(_uaclient())
491
492
async def _uaclient():
493
    parser = argparse.ArgumentParser(description="Connect to server and start python shell. root and objects nodes are available. Node specificed in command line is available as mynode variable")
494
    add_common_args(parser)
495
    parser.add_argument("-c",
496
                        "--certificate",
497
                        help="set client certificate")
498
    parser.add_argument("-k",
499
                        "--private_key",
500
                        help="set client private key")
501
    args = parse_args(parser)
502
503
    client = Client(args.url, timeout=args.timeout)
504
    await _configure_client_with_args(client, args)
505
    if args.certificate:
506
        client.load_client_certificate(args.certificate)
507
    if args.private_key:
508
        client.load_private_key(args.private_key)
509
510
    try:
511
        async with client:
512
            mynode = await get_node(client, args)
513
            # embed()
514
    except (OSError, concurrent.futures._base.TimeoutError) as e:
515
        print(e)
516
        sys.exit(1)
517
518
    sys.exit(0)
519
520
521
async def _uaserver():
522
    parser = argparse.ArgumentParser(description="Run an example OPC-UA server. By importing xml definition and using uawrite command line, it is even possible to expose real data using this server")
523
    # we setup a server, this is a bit different from other tool so we do not reuse common arguments
524
    parser.add_argument("-u",
525
                        "--url",
526
                        help="URL of OPC UA server, default is opc.tcp://0.0.0.0:4840",
527
                        default='opc.tcp://0.0.0.0:4840',
528
                        metavar="URL")
529
    parser.add_argument("-v",
530
                        "--verbose",
531
                        dest="loglevel",
532
                        choices=['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'],
533
                        default='WARNING',
534
                        help="Set log level")
535
    parser.add_argument("-x",
536
                        "--xml",
537
                        metavar="XML_FILE",
538
                        help="Populate address space with nodes defined in XML")
539
    parser.add_argument("-p",
540
                        "--populate",
541
                        action="store_true",
542
                        help="Populate address space with some sample nodes")
543
    parser.add_argument("-c",
544
                        "--disable-clock",
545
                        action="store_true",
546
                        help="Disable clock, to avoid seeing many write if debugging an application")
547
    parser.add_argument("-s",
548
                        "--shell",
549
                        action="store_true",
550
                        help="Start python shell instead of randomly changing node values")
551
    parser.add_argument("--certificate",
552
                        help="set server certificate")
553
    parser.add_argument("--private_key",
554
                        help="set server private key")
555
    args = parser.parse_args()
556
    logging.basicConfig(format="%(levelname)s: %(message)s", level=getattr(logging, args.loglevel))
557
558
    server = Server()
559
    server.set_endpoint(args.url)
560
    if args.certificate:
561
        server.load_certificate(args.certificate)
562
    if args.private_key:
563
        server.load_private_key(args.private_key)
564
    server.disable_clock(args.disable_clock)
565
    server.set_server_name("FreeOpcUa Example Server")
566
    if args.xml:
567
        server.import_xml(args.xml)
568
    if args.populate:
569
        @uamethod
570
        def multiply(parent, x, y):
571
            print("multiply method call with parameters: ", x, y)
572
            return x * y
573
574
        uri = "http://examples.freeopcua.github.io"
575
        idx = server.register_namespace(uri)
576
        objects = server.nodes.objects
577
        myobj = objects.add_object(idx, "MyObject")
578
        mywritablevar = myobj.add_variable(idx, "MyWritableVariable", 6.7)
579
        mywritablevar.set_writable()    # Set MyVariable to be writable by clients
580
        myvar = myobj.add_variable(idx, "MyVariable", 6.7)
581
        myarrayvar = myobj.add_variable(idx, "MyVarArray", [6.7, 7.9])
582
        myprop = myobj.add_property(idx, "MyProperty", "I am a property")
583
        mymethod = myobj.add_method(idx, "MyMethod", multiply, [ua.VariantType.Double, ua.VariantType.Int64], [ua.VariantType.Double])
584
585
    try:
586
        await server.init()
587
        async with server:
588
589
            if args.shell:
590
                embed()
591
            elif args.populate:
592
                count = 0
593
                while True:
594
                    await asyncio.sleep(1)
595
                    myvar.write_value(math.sin(count / 10))
0 ignored issues
show
introduced by
The variable myvar does not seem to be defined in case args.populate on line 568 is False. Are you sure this can never be the case?
Loading history...
596
                    myarrayvar.write_value([math.sin(count / 10), math.sin(count / 100)])
0 ignored issues
show
introduced by
The variable myarrayvar does not seem to be defined in case args.populate on line 568 is False. Are you sure this can never be the case?
Loading history...
597
                    count += 1
598
            else:
599
                while True:
600
                    await asyncio.sleep(1)
601
    except OSError as e:
602
        print(e)
603
        sys.exit(1)
604
    except KeyboardInterrupt:
605
        pass
606
    sys.exit(0)
607
608
def uaserver():
609
    run(_uaserver())
610
611
612
def uadiscover():
613
    run(_uadiscover())
614
615
616
async def _uadiscover():
617
    parser = argparse.ArgumentParser(description="Performs OPC UA discovery and prints information on servers and endpoints.")
618
    add_minimum_args(parser)
619
    parser.add_argument("-n",
620
                        "--network",
621
                        action="store_true",
622
                        help="Also send a FindServersOnNetwork request to server")
623
    # parser.add_argument("-s",
624
                        # "--servers",
625
                        # action="store_false",
626
                        # help="send a FindServers request to server")
627
    # parser.add_argument("-e",
628
                        # "--endpoints",
629
                        # action="store_false",
630
                        # help="send a GetEndpoints request to server")
631
    args = parse_args(parser)
632
633
    client = Client(args.url, timeout=args.timeout)
634
635
    try:
636
        if args.network:
637
            print(f"Performing discovery at {args.url}\n")
638
            for i, server in enumerate(await client.connect_and_find_servers_on_network(), start=1):
639
                print(f'Server {i}:')
640
                # for (n, v) in application_to_strings(server):
641
                    # print('  {}: {}'.format(n, v))
642
                print('')
643
644
        print(f"Performing discovery at {args.url}\n")
645
        for i, server in enumerate(await client.connect_and_find_servers(), start=1):
646
            print(f'Server {i}:')
647
            for (n, v) in application_to_strings(server):
648
                print(f'  {n}: {v}')
649
            print('')
650
651
        for i, ep in enumerate(await client.connect_and_get_server_endpoints(), start=1):
652
            print(f'Endpoint {i}:')
653
            for (n, v) in endpoint_to_strings(ep):
654
                print(f'  {n}: {v}')
655
            print('')
656
    except (OSError, concurrent.futures._base.TimeoutError) as e:
657
        print(e)
658
        sys.exit(1)
659
660
    sys.exit(0)
661
662
663
def print_history(o):
664
    print("{0:30} {1:10} {2}".format('Source timestamp', 'Status', 'Value'))
665
    for d in o:
666
        print("{0:30} {1:10} {2}".format(str(d.SourceTimestamp), d.StatusCode.name, d.Value.Value))
667
668
669
def str_to_datetime(s, default=None):
670
    if not s:
671
        if default is not None:
672
            return default
673
        return datetime.utcnow()
674
    # FIXME: try different datetime formats
675
    for fmt in ["%Y-%m-%d", "%Y-%m-%d %H:%M", "%Y-%m-%d %H:%M:%S"]:
676
        try:
677
            return datetime.strptime(s, fmt)
678
        except ValueError:
679
            pass
680
681
682
def uahistoryread():
683
    run(_uahistoryread())
684
685
686
async def _uahistoryread():
687
    parser = argparse.ArgumentParser(description="Read history of a node")
688
    add_common_args(parser)
689
    parser.add_argument("--starttime",
690
                        default=None,
691
                        help="Start time, formatted as YYYY-MM-DD [HH:MM[:SS]]. Default: current time - one day")
692
    parser.add_argument("--endtime",
693
                        default=None,
694
                        help="End time, formatted as YYYY-MM-DD [HH:MM[:SS]]. Default: current time")
695
    parser.add_argument("-e",
696
                        "--events",
697
                        action="store_true",
698
                        help="Read event history instead of data change history")
699
    parser.add_argument("-l",
700
                        "--limit",
701
                        type=int,
702
                        default=10,
703
                        help="Maximum number of notfication to return")
704
705
    args = parse_args(parser, requirenodeid=True)
706
707
    client = Client(args.url, timeout=args.timeout)
708
    await _configure_client_with_args(client, args)
709
    await client.connect()
710
    try:
711
        node = await get_node(client, args)
712
        starttime = str_to_datetime(args.starttime, datetime.utcnow() - timedelta(days=1))
713
        endtime = str_to_datetime(args.endtime, datetime.utcnow())
714
        print(f"Reading raw history of node {node} at {args.url}; start at {starttime}, end at {endtime}\n")
715
        if args.events:
716
            evs = await node.read_event_history(starttime, endtime, numvalues=args.limit)
717
            for ev in evs:
718
                print(ev)
719
        else:
720
            print_history(await node.read_raw_history(starttime, endtime, numvalues=args.limit))
721
    except Exception as e:
722
        print(e)
723
        sys.exit(1)
724
    finally:
725
        await client.disconnect()
726
    sys.exit(0)
727
728
729
def uacall():
730
    run(_uacall())
731
732
733
async def _uacall():
734
    parser = argparse.ArgumentParser(description="Call method of a node")
735
    add_common_args(parser)
736
    parser.add_argument("-m",
737
                        "--method",
738
                        dest="method",
739
                        type=str,
740
                        default=None,
741
                        help="browse name of method to call")
742
    parser.add_argument("-t",
743
                        "--datatype",
744
                        dest="datatype",
745
                        default="guess",
746
                        choices=["guess", 'byte', 'sbyte', 'nodeid', 'expandednodeid', 'qualifiedname', 'browsename', 'string', 'float', 'double', 'int16', 'int32', "int64", 'uint16', 'uint32', 'uint64', "bool", "string", 'datetime', 'bytestring', 'xmlelement', 'statuscode', 'localizedtext'],
747
                        help="Data type to return")
748
    parser.add_argument("-l",
749
                        "--list",
750
                        "--array",
751
                        dest="array",
752
                        default="guess",
753
                        choices=["guess", "true", "false"],
754
                        help="Value is an array")
755
    parser.add_argument("value",
756
                        help="Comma separated value(s) to use for call to method, if any",
757
                        nargs="?",
758
                        metavar="VALUE")
759
760
    args = parse_args(parser, requirenodeid=True)
761
762
    client = Client(args.url, timeout=args.timeout)
763
    await _configure_client_with_args(client, args)
764
    await client.connect()
765
    try:
766
        node = await get_node(client, args)
767
        if args.value is None:
768
            val = ()  # empty tuple
769
        else:
770
            val = args.value.split(",")
771
            val = [_val_to_variant(v, args) for v in val]
772
773
        method_id = None
774
775
        if args.method is not None:
776
            method_id = args.method
777
        else:
778
            methods = await node.get_methods()
779
            if len(methods) == 0:
780
                raise ValueError("No methods in selected node and no method given")
781
            else:
782
                method_id = methods[0]
783
        result = await node.call_method(method_id, *val)
784
        print(f"resulting result_variants={result}")
785
    except Exception as e:
786
        print(e)
787
        sys.exit(1)
788
    finally:
789
        await client.disconnect()
790
    sys.exit(0)
791
792
793
def uageneratestructs():
794
    run(_uageneratestructs())
795
796
797
async def _uageneratestructs():
798
    parser = argparse.ArgumentParser(description="Generate a Python module from the xml structure definition (.bsd), the node argument is typically a children of i=93")
799
    add_common_args(parser, require_node=True)
800
    parser.add_argument("-o",
801
                        "--output",
802
                        dest="output_path",
803
                        required=True,
804
                        type=str,
805
                        default=None,
806
                        help="The python file to be generated.",
807
                        )
808
    args = parse_args(parser, requirenodeid=True)
809
810
    client = Client(args.url, timeout=args.timeout)
811
    await _configure_client_with_args(client, args)
812
    await client.connect()
813
    try:
814
        node = await get_node(client, args)
815
        generators, _ = await client.load_type_definitions([node])
816
        generators[0].save_to_file(args.output_path, True)
817
    finally:
818
        await client.disconnect()
819