Completed
Pull Request — master (#140)
by Olivier
02:43
created

opcua.ua.pack_string()   A

Complexity

Conditions 4

Size

Total Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 4.0218
Metric Value
cc 4
dl 0
loc 9
ccs 8
cts 9
cp 0.8889
crap 4.0218
rs 9.2
1
"""
2
implement ua datatypes
3
"""
4 1
import logging
5 1
from enum import Enum, IntEnum
6 1
from datetime import datetime, timedelta, tzinfo, MAXYEAR
7 1
from calendar import timegm
8 1
import sys
9 1
import os
10 1
import uuid
11 1
import struct
12 1
import re
13 1
if sys.version_info.major > 2:
14 1
    unicode = str
15
16 1
from opcua.ua import status_codes
17 1
from opcua.common.uaerrors import UaError
18 1
from opcua.common.uaerrors import UaStatusCodeError
19 1
from opcua.common.uaerrors import UaStringParsingError
20
21
22 1
logger = logging.getLogger('opcua.uaprotocol')
23
24
25
# types that will packed and unpacked directly using struct (string, bytes and datetime are handles as special cases
26 1
UaTypes = ("Boolean", "SByte", "Byte", "Int8", "UInt8", "Int16", "UInt16", "Int32", "UInt32", "Int64", "UInt64", "Float", "Double")
27
28
29 1
EPOCH_AS_FILETIME = 116444736000000000  # January 1, 1970 as MS file time
30 1
HUNDREDS_OF_NANOSECONDS = 10000000
31 1
FILETIME_EPOCH_AS_DATETIME = datetime(1601, 1, 1)
32
33
34 1
class UTC(tzinfo):
35
36
    """UTC"""
37
38 1
    def utcoffset(self, dt):
39
        return timedelta(0)
40
41 1
    def tzname(self, dt):
42
        return "UTC"
43
44 1
    def dst(self, dt):
45 1
        return timedelta(0)
46
47
48
# method copied from David Buxton <[email protected]> sample code
49 1
def datetime_to_win_epoch(dt):
50 1
    if (dt.tzinfo is None) or (dt.tzinfo.utcoffset(dt) is None):
51 1
        dt = dt.replace(tzinfo=UTC())
52 1
    ft = EPOCH_AS_FILETIME + (timegm(dt.timetuple()) * HUNDREDS_OF_NANOSECONDS)
53 1
    return ft + (dt.microsecond * 10)
54
55
56 1
def win_epoch_to_datetime(epch):
57 1
    try:
58 1
        return FILETIME_EPOCH_AS_DATETIME + timedelta(microseconds=epch // 10)
59
    except OverflowError:
60
        # FILETIMEs after 31 Dec 9999 can't be converted to datetime
61
        logger.warning("datetime overflow: {}".format(epch))
62
        return datetime(MAXYEAR, 12, 31, 23, 59, 59, 999999)
63
64
65
#  minimum datetime as in ua spec, used for history
66 1
DateTimeMinValue = datetime(1606, 1, 1, 12, 0, 0)
67
68
69 1
def build_array_format_py2(prefix, length, fmtchar):
70
    return prefix + str(length) + fmtchar
71
72
73 1
def build_array_format_py3(prefix, length, fmtchar):
74 1
    return prefix + str(length) + chr(fmtchar)
75
76
77 1
if sys.version_info.major < 3:
78
    build_array_format = build_array_format_py2
79
else:
80 1
    build_array_format = build_array_format_py3
81
82
83 1
def pack_uatype_array_primitive(st, value, length):
84 1
    if length == 1:
85 1
        return b'\x01\x00\x00\x00' + st.pack(value[0])
86
    else:
87 1
        return struct.pack(build_array_format("<i", length, st.format[1]), length, *value)
88
89
90 1
def pack_uatype_array(uatype, value):
91 1
    if value is None:
92
        return b'\xff\xff\xff\xff'
93 1
    length = len(value)
94 1
    if length == 0:
95 1
        return b'\x00\x00\x00\x00'
96 1
    if uatype in uatype2struct:
97 1
        return pack_uatype_array_primitive(uatype2struct[uatype], value, length)
98 1
    b = []
99 1
    b.append(uatype_Int32.pack(length))
100 1
    for val in value:
101 1
        b.append(pack_uatype(uatype, val))
102 1
    return b"".join(b)
103
104
105 1
def pack_uatype(uatype, value):
106 1
    if uatype in uatype2struct:
107 1
        if value is None:
108
            value = 0
109 1
        return uatype2struct[uatype].pack(value)
110 1
    elif uatype == "Null":
111 1
        return b''
112 1
    elif uatype == "String":
113 1
        return pack_string(value)
114 1
    elif uatype in ("CharArray", "ByteString", "Custom"):
115 1
        return pack_bytes(value)
116 1
    elif uatype == "DateTime":
117 1
        return pack_datetime(value)
118 1
    elif uatype == "ExtensionObject":
119
        # dependency loop: classes in uaprotocol_auto use Variant defined in this file,
120
        # but Variant can contain any object from uaprotocol_auto as ExtensionObject.
121
        # Using local import to avoid import loop
122 1
        from opcua.ua.uaprotocol_auto import extensionobject_to_binary
123 1
        return extensionobject_to_binary(value)
124
    else:
125 1
        return value.to_binary()
126
127 1
uatype_Int8 = struct.Struct("<b")
128 1
uatype_SByte = uatype_Int8
129 1
uatype_Int16 = struct.Struct("<h")
130 1
uatype_Int32 = struct.Struct("<i")
131 1
uatype_Int64 = struct.Struct("<q")
132 1
uatype_UInt8 = struct.Struct("<B")
133 1
uatype_Char = uatype_UInt8
134 1
uatype_Byte = uatype_UInt8
135 1
uatype_UInt16 = struct.Struct("<H")
136 1
uatype_UInt32 = struct.Struct("<I")
137 1
uatype_UInt64 = struct.Struct("<Q")
138 1
uatype_Boolean = struct.Struct("<?")
139 1
uatype_Double = struct.Struct("<d")
140 1
uatype_Float = struct.Struct("<f")
141
142 1
uatype2struct = {
143
    "Int8": uatype_Int8,
144
    "SByte": uatype_SByte,
145
    "Int16": uatype_Int16,
146
    "Int32": uatype_Int32,
147
    "Int64": uatype_Int64,
148
    "UInt8": uatype_UInt8,
149
    "Char": uatype_Char,
150
    "Byte": uatype_Byte,
151
    "UInt16": uatype_UInt16,
152
    "UInt32": uatype_UInt32,
153
    "UInt64": uatype_UInt64,
154
    "Boolean": uatype_Boolean,
155
    "Double": uatype_Double,
156
    "Float": uatype_Float,
157
}
158
159
160 1
def unpack_uatype(uatype, data):
161 1
    if uatype in uatype2struct:
162 1
        st = uatype2struct[uatype]
163 1
        return st.unpack(data.read(st.size))[0]
164 1
    elif uatype == "String":
165 1
        return unpack_string(data)
166 1
    elif uatype in ("CharArray", "ByteString", "Custom"):
167 1
        return unpack_bytes(data)
168 1
    elif uatype == "DateTime":
169 1
        return unpack_datetime(data)
170 1
    elif uatype == "ExtensionObject":
171
        # dependency loop: classes in uaprotocol_auto use Variant defined in this file,
172
        # but Variant can contain any object from uaprotocol_auto as ExtensionObject.
173
        # Using local import to avoid import loop
174 1
        from opcua.ua.uaprotocol_auto import extensionobject_from_binary
175 1
        return extensionobject_from_binary(data)
176
    else:
177 1
        glbs = globals()
178 1
        if uatype in glbs:
179 1
            klass = glbs[uatype]
180 1
            if hasattr(klass, 'from_binary'):
181 1
                return klass.from_binary(data)
182
        raise UaError("can not unpack unknown uatype %s" % uatype)
183
184
185 1
def unpack_uatype_array(uatype, data):
186 1
    length = uatype_Int32.unpack(data.read(4))[0]
187 1
    if length == -1:
188
        return None
189 1
    elif length == 0:
190 1
        return []
191 1
    elif uatype in uatype2struct:
192 1
        st = uatype2struct[uatype]
193 1
        if length == 1:
194 1
            return list(st.unpack(data.read(st.size)))
195
        else:
196 1
            arrst = struct.Struct(build_array_format("<", length, st.format[1]))
197 1
            return list(arrst.unpack(data.read(arrst.size)))
198
    else:
199 1
        result = []
200 1
        for _ in range(0, length):
201 1
            result.append(unpack_uatype(uatype, data))
202 1
        return result
203
204
205 1
def pack_datetime(value):
206 1
    epch = datetime_to_win_epoch(value)
207 1
    return uatype_Int64.pack(epch)
208
209
210 1
def unpack_datetime(data):
211 1
    epch = uatype_Int64.unpack(data.read(8))[0]
212 1
    return win_epoch_to_datetime(epch)
213
214
215 1
def pack_string(string):
216 1
    if string is None:
217
        return uatype_Int32.pack(-1)
218 1
    if isinstance(string, unicode):
219 1
        string = string.encode('utf-8')
220 1
    length = len(string)
221 1
    if length == 0:
222 1
        return b'\xff\xff\xff\xff'
223 1
    return uatype_Int32.pack(length) + string
224
225 1
pack_bytes = pack_string
226
227
228 1
def unpack_bytes(data):
229 1
    length = uatype_Int32.unpack(data.read(4))[0]
230 1
    if length == -1:
231
        # FIXME: return None, check it does not break things
232 1
        return b''
233 1
    return data.read(length)
234
235
236 1
def py3_unpack_string(data):
237 1
    b = unpack_bytes(data)
238 1
    return b.decode("utf-8")
239
240
241 1
if sys.version_info.major < 3:
242
    unpack_string = unpack_bytes
243
else:
244 1
    unpack_string = py3_unpack_string
245
246
247 1
def test_bit(data, offset):
248 1
    mask = 1 << offset
249 1
    return data & mask
250
251
252 1
def set_bit(data, offset):
253 1
    mask = 1 << offset
254 1
    return data | mask
255
256
257 1
def unset_bit(data, offset):
258 1
    mask = 1 << offset
259 1
    return data & ~mask
260
261
262 1
class _FrozenClass(object):
263
264
    """
265
    make it impossible to add members to a class.
266
    This is a hack since I found out that most bugs are due to misspelling a variable in protocol
267
    """
268 1
    _freeze = False
269
270 1
    def __setattr__(self, key, value):
271 1
        if self._freeze and not hasattr(self, key):
272
            raise TypeError("Error adding member '{}' to class '{}', class is frozen, members are {}".format(key, self.__class__.__name__, self.__dict__.keys()))
273 1
        object.__setattr__(self, key, value)
274
275 1
if "PYOPCUA_NO_TYPO_CHECK" in os.environ:
276
    # typo check is cpu consuming, but it will make debug easy.
277
    # if typo check is not need (in production), please set env PYOPCUA_NO_TYPO_CHECK.
278
    # this will make all uatype class inherit from object intead of _FrozenClass
279
    # and skip the typo check.
280
    FrozenClass = object
281
else:
282 1
    FrozenClass = _FrozenClass
283
284
285 1
class ValueRank(IntEnum):
286
    """
287
    Defines dimensions of a variable.
288
    This enum does not support all cases since ValueRank support any n>0
289
    but since it is an IntEnum it can be replace by a normal int
290
    """
291 1
    ScalarOrOneDimension = -3
292 1
    Any = -2
293 1
    Scalar = -1
294 1
    OneOrMoreDimensions = 0
295 1
    OneDimension = 1
296
    # the next names are not in spec but so common we express them here
297 1
    TwoDimensions = 2
298 1
    ThreeDimensions = 3
299 1
    FourDimensions = 3
300
301
302 1
class AccessLevel(IntEnum):
303
    """
304
    """
305 1
    CurrentRead = 0
306 1
    CurrentWrite = 1
307 1
    HistoryRead = 2
308 1
    HistoryWrite = 3
309 1
    SemanticChange = 4
310
311
312 1
class AccessLevelMask(IntEnum):
313
    """
314
    Mask for access level
315
    """
316 1
    CurrentRead = 1 << AccessLevel.CurrentRead
317 1
    CurrentWrite = 1 << AccessLevel.CurrentWrite
318 1
    HistoryRead = 1 << AccessLevel.HistoryRead
319 1
    HistoryWrite = 1 << AccessLevel.HistoryWrite
320 1
    SemanticChange = 1 << AccessLevel.SemanticChange
321
322
323 1
class Guid(FrozenClass):
324
325 1
    def __init__(self):
326 1
        self.uuid = uuid.uuid4()
327 1
        self._freeze = True
328
329 1
    def to_binary(self):
330 1
        return self.uuid.bytes
331
332 1
    def __hash__(self):
333
        return hash(self.uuid.bytes)
334
335 1
    @staticmethod
336
    def from_binary(data):
337 1
        g = Guid()
338 1
        g.uuid = uuid.UUID(bytes=data.read(16))
339 1
        return g
340
341 1
    def __eq__(self, other):
342 1
        return isinstance(other, Guid) and self.uuid == other.uuid
343
344
345 1
class StatusCode(FrozenClass):
346
347
    """
348
    :ivar value:
349
    :vartype value: int
350
    :ivar name:
351
    :vartype name: string
352
    :ivar doc:
353
    :vartype doc: string
354
    """
355
356 1
    def __init__(self, value=0):
357 1
        self.value = value
358 1
        self.name, self.doc = status_codes.get_name_and_doc(value)
359 1
        self._freeze = True
360
361 1
    def to_binary(self):
362 1
        return uatype_UInt32.pack(self.value)
363
364 1
    @staticmethod
365
    def from_binary(data):
366 1
        val = uatype_UInt32.unpack(data.read(4))[0]
367 1
        sc = StatusCode(val)
368 1
        return sc
369
370 1
    def check(self):
371
        """
372
        raise en exception if status code is anything else than 0
373
        use is is_good() method if not exception is desired
374
        """
375 1
        if self.value != 0:
376 1
            raise UaStatusCodeError("{}({})".format(self.doc, self.name))
377
378 1
    def is_good(self):
379
        """
380
        return True if status is Good.
381
        """
382 1
        if self.value == 0:
383 1
            return True
384 1
        return False
385
386 1
    def __str__(self):
387
        return 'StatusCode({})'.format(self.name)
388 1
    __repr__ = __str__
389
390 1
    def __eq__(self, other):
391
        return self.value == other.value
392
393 1
    def __ne__(self, other):
394
        return not self.__eq__(other)
395
396
397 1
class NodeIdType(Enum):
398 1
    TwoByte = 0
399 1
    FourByte = 1
400 1
    Numeric = 2
401 1
    String = 3
402 1
    Guid = 4
403 1
    ByteString = 5
404
405
406 1
class NodeId(FrozenClass):
407
408
    """
409
    NodeId Object
410
411
    Args:
412
        identifier: The identifier might be an int, a string, bytes or a Guid
413
        namespaceidx(int): The index of the namespace
414
        nodeidtype(NodeIdType): The type of the nodeid if it cannor be guess or you want something special like twobyte nodeid or fourbytenodeid
415
416
417
    :ivar Identifier:
418
    :vartype Identifier: NodeId
419
    :ivar NamespaceIndex:
420
    :vartype NamespaceIndex: Int
421
    :ivar NamespaceUri:
422
    :vartype NamespaceUri: String
423
    :ivar ServerIndex:
424
    :vartype ServerIndex: Int
425
    """
426
427 1
    def __init__(self, identifier=None, namespaceidx=0, nodeidtype=None):
428 1
        self.Identifier = identifier
429 1
        self.NamespaceIndex = namespaceidx
430 1
        self.NodeIdType = nodeidtype
431 1
        self.NamespaceUri = ""
432 1
        self.ServerIndex = 0
433 1
        self._freeze = True
434 1
        if not isinstance(self.NamespaceIndex, int):
435 1
            raise UaError("NamespaceIndex must be an int")
436 1
        if self.Identifier is None:
437 1
            self.Identifier = 0
438 1
            self.NodeIdType = NodeIdType.TwoByte
439 1
            return
440 1
        if self.NodeIdType is None:
441 1
            if isinstance(self.Identifier, int):
442 1
                self.NodeIdType = NodeIdType.Numeric
443 1
            elif isinstance(self.Identifier, str):
444 1
                self.NodeIdType = NodeIdType.String
445
            elif isinstance(self.Identifier, bytes):
446
                self.NodeIdType = NodeIdType.ByteString
447
            else:
448
                raise UaError("NodeId: Could not guess type of NodeId, set NodeIdType")
449
450 1
    def __key(self):
451 1
        if self.NodeIdType in (NodeIdType.TwoByte, NodeIdType.FourByte, NodeIdType.Numeric):  # twobyte, fourbyte and numeric may represent the same node
452 1
            return self.NamespaceIndex, self.Identifier
453
        else:
454 1
            return self.NodeIdType, self.NamespaceIndex, self.Identifier
455
456 1
    def __eq__(self, node):
457 1
        return isinstance(node, NodeId) and self.__key() == node.__key()
458
459 1
    def __ne__(self, other):
460 1
        return not self.__eq__(other)
461
462 1
    def __hash__(self):
463 1
        return hash(self.__key())
464
465 1
    def is_null(self):
466 1
        if self.NamespaceIndex != 0:
467 1
            return False
468 1
        return self.has_null_identifier()
469
470 1
    def has_null_identifier(self):
471 1
        if not self.Identifier:
472 1
            return True
473 1
        if self.NodeIdType == NodeIdType.Guid and re.match(b'0.', self.Identifier):
474 1
            return True
475 1
        return False
476
477 1
    @staticmethod
478
    def from_string(string):
479 1
        try:
480 1
            return NodeId._from_string(string)
481 1
        except ValueError as ex:
482 1
            raise UaStringParsingError("Error parsing string {}".format(string), ex)
483
484 1
    @staticmethod
485
    def _from_string(string):
486 1
        l = string.split(";")
487 1
        identifier = None
488 1
        namespace = 0
489 1
        ntype = None
490 1
        srv = None
491 1
        nsu = None
492 1
        for el in l:
493 1
            if not el:
494 1
                continue
495 1
            k, v = el.split("=", 1)
496 1
            k = k.strip()
497 1
            v = v.strip()
498 1
            if k == "ns":
499 1
                namespace = int(v)
500 1
            elif k == "i":
501 1
                ntype = NodeIdType.Numeric
502 1
                identifier = int(v)
503 1
            elif k == "s":
504 1
                ntype = NodeIdType.String
505 1
                identifier = v
506 1
            elif k == "g":
507
                ntype = NodeIdType.Guid
508
                identifier = v
509 1
            elif k == "b":
510
                ntype = NodeIdType.ByteString
511
                identifier = v
512 1
            elif k == "srv":
513 1
                srv = v
514
            elif k == "nsu":
515
                nsu = v
516 1
        if identifier is None:
517 1
            raise UaStringParsingError("Could not find identifier in string: " + string)
518 1
        nodeid = NodeId(identifier, namespace, ntype)
519 1
        nodeid.NamespaceUri = nsu
520 1
        nodeid.ServerIndex = srv
521 1
        return nodeid
522
523 1
    def to_string(self):
524 1
        string = ""
525 1
        if self.NamespaceIndex != 0:
526 1
            string += "ns={};".format(self.NamespaceIndex)
527 1
        ntype = None
528 1
        if self.NodeIdType == NodeIdType.Numeric:
529 1
            ntype = "i"
530 1
        elif self.NodeIdType == NodeIdType.String:
531 1
            ntype = "s"
532 1
        elif self.NodeIdType == NodeIdType.TwoByte:
533 1
            ntype = "i"
534 1
        elif self.NodeIdType == NodeIdType.FourByte:
535 1
            ntype = "i"
536
        elif self.NodeIdType == NodeIdType.Guid:
537
            ntype = "g"
538
        elif self.NodeIdType == NodeIdType.ByteString:
539
            ntype = "b"
540 1
        string += "{}={}".format(ntype, self.Identifier)
541 1
        if self.ServerIndex:
542
            string = "srv=" + str(self.ServerIndex) + string
543 1
        if self.NamespaceUri:
544
            string += "nsu={}".format(self.NamespaceUri)
545 1
        return string
546
547 1
    def __str__(self):
548 1
        return "{}NodeId({})".format(self.NodeIdType.name, self.to_string())
549 1
    __repr__ = __str__
550
551 1
    def to_binary(self):
552 1
        if self.NodeIdType == NodeIdType.TwoByte:
553 1
            return struct.pack("<BB", self.NodeIdType.value, self.Identifier)
554 1
        elif self.NodeIdType == NodeIdType.FourByte:
555 1
            return struct.pack("<BBH", self.NodeIdType.value, self.NamespaceIndex, self.Identifier)
556 1
        elif self.NodeIdType == NodeIdType.Numeric:
557 1
            return struct.pack("<BHI", self.NodeIdType.value, self.NamespaceIndex, self.Identifier)
558 1
        elif self.NodeIdType == NodeIdType.String:
559 1
            return struct.pack("<BH", self.NodeIdType.value, self.NamespaceIndex) + \
560
                pack_string(self.Identifier)
561 1
        elif self.NodeIdType == NodeIdType.ByteString:
562 1
            return struct.pack("<BH", self.NodeIdType.value, self.NamespaceIndex) + \
563
                pack_bytes(self.Identifier)
564
        else:
565 1
            return struct.pack("<BH", self.NodeIdType.value, self.NamespaceIndex) + \
566
                self.Identifier.to_binary()
567
        #FIXME: Missing NNamespaceURI and ServerIndex
568
569 1
    @staticmethod
570
    def from_binary(data):
571 1
        nid = NodeId()
572 1
        encoding = ord(data.read(1))
573 1
        nid.NodeIdType = NodeIdType(encoding & 0b00111111)
574
575 1
        if nid.NodeIdType == NodeIdType.TwoByte:
576 1
            nid.Identifier = ord(data.read(1))
577 1
        elif nid.NodeIdType == NodeIdType.FourByte:
578 1
            nid.NamespaceIndex, nid.Identifier = struct.unpack("<BH", data.read(3))
579 1
        elif nid.NodeIdType == NodeIdType.Numeric:
580 1
            nid.NamespaceIndex, nid.Identifier = struct.unpack("<HI", data.read(6))
581 1
        elif nid.NodeIdType == NodeIdType.String:
582 1
            nid.NamespaceIndex = uatype_UInt16.unpack(data.read(2))[0]
583 1
            nid.Identifier = unpack_string(data)
584 1
        elif nid.NodeIdType == NodeIdType.ByteString:
585 1
            nid.NamespaceIndex = uatype_UInt16.unpack(data.read(2))[0]
586 1
            nid.Identifier = unpack_bytes(data)
587 1
        elif nid.NodeIdType == NodeIdType.Guid:
588 1
            nid.NamespaceIndex = uatype_UInt16.unpack(data.read(2))[0]
589 1
            nid.Identifier = Guid.from_binary(data)
590
        else:
591
            raise UaError("Unknown NodeId encoding: " + str(nid.NodeIdType))
592
593 1
        if test_bit(encoding, 7):
594
            nid.NamespaceUri = unpack_string(data)
595 1
        if test_bit(encoding, 6):
596
            nid.ServerIndex = uatype_UInt32.unpack(data.read(4))[0]
597
598 1
        return nid
599
600
601 1
class TwoByteNodeId(NodeId):
602
603 1
    def __init__(self, identifier):
604 1
        NodeId.__init__(self, identifier, 0, NodeIdType.TwoByte)
605
606
607 1
class FourByteNodeId(NodeId):
608
609 1
    def __init__(self, identifier, namespace=0):
610 1
        NodeId.__init__(self, identifier, namespace, NodeIdType.FourByte)
611
612
613 1
class NumericNodeId(NodeId):
614
615 1
    def __init__(self, identifier, namespace=0):
616 1
        NodeId.__init__(self, identifier, namespace, NodeIdType.Numeric)
617
618
619 1
class ByteStringNodeId(NodeId):
620
621 1
    def __init__(self, identifier, namespace=0):
622 1
        NodeId.__init__(self, identifier, namespace, NodeIdType.ByteString)
623
624
625 1
class GuidNodeId(NodeId):
626
627 1
    def __init__(self, identifier, namespace=0):
628 1
        NodeId.__init__(self, identifier, namespace, NodeIdType.Guid)
629
630
631 1
class StringNodeId(NodeId):
632
633 1
    def __init__(self, identifier, namespace=0):
634 1
        NodeId.__init__(self, identifier, namespace, NodeIdType.String)
635
636
637 1
ExpandedNodeId = NodeId
638
639
640 1
class QualifiedName(FrozenClass):
641
642
    '''
643
    A string qualified with a namespace index.
644
    '''
645
646 1
    def __init__(self, name="", namespaceidx=0):
647 1
        if not isinstance(namespaceidx, int):
648
            raise UaError("namespaceidx must be an int")
649 1
        self.NamespaceIndex = namespaceidx
650 1
        self.Name = name
651 1
        self._freeze = True
652
653 1
    def to_string(self):
654 1
        return "{}:{}".format(self.NamespaceIndex, self.Name)
655
656 1
    @staticmethod
657
    def from_string(string):
658 1
        if ":" in string:
659 1
            try:
660 1
                idx, name = string.split(":", 1)
661 1
                idx = int(idx)
662 1
            except (TypeError, ValueError) as ex:
663 1
                raise UaStringParsingError("Error parsing string {}".format(string), ex)
664
        else:
665 1
            idx = 0
666 1
            name = string
667 1
        return QualifiedName(name, idx)
668
669 1
    def to_binary(self):
670 1
        packet = []
671 1
        packet.append(uatype_UInt16.pack(self.NamespaceIndex))
672 1
        packet.append(pack_string(self.Name))
673 1
        return b''.join(packet)
674
675 1
    @staticmethod
676
    def from_binary(data):
677 1
        obj = QualifiedName()
678 1
        obj.NamespaceIndex = uatype_UInt16.unpack(data.read(2))[0]
679 1
        obj.Name = unpack_string(data)
680 1
        return obj
681
682 1
    def __eq__(self, bname):
683 1
        return isinstance(bname, QualifiedName) and self.Name == bname.Name and self.NamespaceIndex == bname.NamespaceIndex
684
685 1
    def __ne__(self, other):
686
        return not self.__eq__(other)
687
688 1
    def __str__(self):
689
        return 'QualifiedName({}:{})'.format(self.NamespaceIndex, self.Name)
690
691 1
    __repr__ = __str__
692
693
694 1
class LocalizedText(FrozenClass):
695
696
    '''
697
    A string qualified with a namespace index.
698
    '''
699
700 1
    def __init__(self, text=""):
701 1
        self.Encoding = 0
702 1
        self.Text = text
703 1
        if isinstance(self.Text, unicode):
704 1
            self.Text = self.Text.encode('utf-8')
705 1
        if self.Text:
706 1
            self.Encoding |= (1 << 1)
707 1
        self.Locale = b''
708 1
        self._freeze = True
709
710 1
    def to_binary(self):
711 1
        packet = []
712 1
        if self.Locale:
713
            self.Encoding |= (1 << 0)
714 1
        if self.Text:
715 1
            self.Encoding |= (1 << 1)
716 1
        packet.append(uatype_UInt8.pack(self.Encoding))
717 1
        if self.Locale:
718
            packet.append(pack_bytes(self.Locale))
719 1
        if self.Text:
720 1
            packet.append(pack_bytes(self.Text))
721 1
        return b''.join(packet)
722
723 1
    @staticmethod
724
    def from_binary(data):
725 1
        obj = LocalizedText()
726 1
        obj.Encoding = ord(data.read(1))
727 1
        if obj.Encoding & (1 << 0):
728
            obj.Locale = unpack_bytes(data)
729 1
        if obj.Encoding & (1 << 1):
730 1
            obj.Text = unpack_bytes(data)
731 1
        return obj
732
733 1
    def to_string(self):
734
        # FIXME: use local
735
        return self.Text.decode()
736
737 1
    def __str__(self):
738
        return 'LocalizedText(' + 'Encoding:' + str(self.Encoding) + ', ' + \
739
            'Locale:' + str(self.Locale) + ', ' + \
740
            'Text:' + str(self.Text) + ')'
741 1
    __repr__ = __str__
742
743 1
    def __eq__(self, other):
744 1
        if isinstance(other, LocalizedText) and self.Locale == other.Locale and self.Text == other.Text:
745 1
            return True
746 1
        return False
747
748 1
    def __ne__(self, other):
749 1
        return not self.__eq__(other)
750
751
752 1
class ExtensionObject(FrozenClass):
753
754
    '''
755
756
    Any UA object packed as an ExtensionObject
757
758
759
    :ivar TypeId:
760
    :vartype TypeId: NodeId
761
    :ivar Body:
762
    :vartype Body: bytes
763
764
    '''
765
766 1
    def __init__(self):
767 1
        self.TypeId = NodeId()
768 1
        self.Encoding = 0
769 1
        self.Body = b''
770 1
        self._freeze = True
771
772 1
    def to_binary(self):
773 1
        packet = []
774 1
        if self.Body:
775 1
            self.Encoding |= (1 << 0)
776 1
        packet.append(self.TypeId.to_binary())
777 1
        packet.append(pack_uatype('UInt8', self.Encoding))
778 1
        if self.Body:
779 1
            packet.append(pack_uatype('ByteString', self.Body))
780 1
        return b''.join(packet)
781
782 1
    @staticmethod
783
    def from_binary(data):
784
        obj = ExtensionObject()
785
        obj.TypeId = NodeId.from_binary(data)
786
        obj.Encoding = unpack_uatype('UInt8', data)
787
        if obj.Encoding & (1 << 0):
788
            obj.Body = unpack_uatype('ByteString', data)
789
        return obj
790
791 1
    @staticmethod
792
    def from_object(obj):
793
        ext = ExtensionObject()
794
        oid = getattr(ObjectIds, "{}_Encoding_DefaultBinary".format(obj.__class__.__name__))
795
        ext.TypeId = FourByteNodeId(oid)
796
        ext.Body = obj.to_binary()
797
        return ext
798
799 1
    def __str__(self):
800
        return 'ExtensionObject(' + 'TypeId:' + str(self.TypeId) + ', ' + \
801
            'Encoding:' + str(self.Encoding) + ', ' + str(len(self.Body)) + ' bytes)'
802
803 1
    __repr__ = __str__
804
805
806 1
class VariantType(Enum):
807
808
    '''
809
    The possible types of a variant.
810
811
    :ivar Null:
812
    :ivar Boolean:
813
    :ivar SByte:
814
    :ivar Byte:
815
    :ivar Int16:
816
    :ivar UInt16:
817
    :ivar Int32:
818
    :ivar UInt32:
819
    :ivar Int64:
820
    :ivar UInt64:
821
    :ivar Float:
822
    :ivar Double:
823
    :ivar String:
824
    :ivar DateTime:
825
    :ivar Guid:
826
    :ivar ByteString:
827
    :ivar XmlElement:
828
    :ivar NodeId:
829
    :ivar ExpandedNodeId:
830
    :ivar StatusCode:
831
    :ivar QualifiedName:
832
    :ivar LocalizedText:
833
    :ivar ExtensionObject:
834
    :ivar DataValue:
835
    :ivar Variant:
836
    :ivar DiagnosticInfo:
837
838
839
840
    '''
841 1
    Null = 0
842 1
    Boolean = 1
843 1
    SByte = 2
844 1
    Byte = 3
845 1
    Int16 = 4
846 1
    UInt16 = 5
847 1
    Int32 = 6
848 1
    UInt32 = 7
849 1
    Int64 = 8
850 1
    UInt64 = 9
851 1
    Float = 10
852 1
    Double = 11
853 1
    String = 12
854 1
    DateTime = 13
855 1
    Guid = 14
856 1
    ByteString = 15
857 1
    XmlElement = 16
858 1
    NodeId = 17
859 1
    ExpandedNodeId = 18
860 1
    StatusCode = 19
861 1
    QualifiedName = 20
862 1
    LocalizedText = 21
863 1
    ExtensionObject = 22
864 1
    DataValue = 23
865 1
    Variant = 24
866 1
    DiagnosticInfo = 25
867
868
869 1
class VariantTypeCustom(object):
870 1
    def __init__(self, val):
871 1
        self.name = "Custom"
872 1
        self.value = val
873 1
        if self.value > 0b00111111:
874 1
            raise UaError("Cannot create VariantType. VariantType must be %s > x > %s", 0b111111, 25)
875
876 1
    def __str__(self):
877
        return "VariantType.Custom:{}".format(self.value)
878 1
    __repr__ = __str__
879
880 1
    def __eq__(self, other):
881 1
        return self.value == other.value
882
883
884 1
class Variant(FrozenClass):
885
886
    """
887
    Create an OPC-UA Variant object.
888
    if no argument a Null Variant is created.
889
    if not variant type is given, attemps to guess type from python type
890
    if a variant is given as value, the new objects becomes a copy of the argument
891
892
    :ivar Value:
893
    :vartype Value: Any supported type
894
    :ivar VariantType:
895
    :vartype VariantType: VariantType
896
    """
897
898 1
    def __init__(self, value=None, varianttype=None, dimensions=None):
899 1
        self.Value = value
900 1
        self.VariantType = varianttype
901 1
        self.Dimensions = dimensions
902 1
        self._freeze = True
903 1
        if isinstance(value, Variant):
904 1
            self.Value = value.Value
905 1
            self.VariantType = value.VariantType
906 1
        if self.VariantType is None:
907 1
            self.VariantType = self._guess_type(self.Value)
908 1
        if self.Dimensions is None and type(self.Value) in (list, tuple):
909 1
            dims = get_shape(self.Value)
910 1
            if len(dims) > 1:
911 1
                self.Dimensions = dims
912
913 1
    def __eq__(self, other):
914 1
        if isinstance(other, Variant) and self.VariantType == other.VariantType and self.Value == other.Value:
915 1
            return True
916 1
        return False
917
918 1
    def __ne__(self, other):
919 1
        return not self.__eq__(other)
920
921 1
    def _guess_type(self, val):
922 1
        if isinstance(val, (list, tuple)):
923 1
            error_val = val
924 1
        while isinstance(val, (list, tuple)):
925 1
            if len(val) == 0:
926
                raise UaError("could not guess UA type of variable {}".format(error_val))
927 1
            val = val[0]
928 1
        if val is None:
929 1
            return VariantType.Null
930 1
        elif isinstance(val, bool):
931 1
            return VariantType.Boolean
932 1
        elif isinstance(val, float):
933 1
            return VariantType.Double
934 1
        elif isinstance(val, int):
935 1
            return VariantType.Int64
936 1
        elif type(val) in (str, unicode):
937 1
            return VariantType.String
938 1
        elif isinstance(val, bytes):
939 1
            return VariantType.ByteString
940 1
        elif isinstance(val, datetime):
941 1
            return VariantType.DateTime
942
        else:
943 1
            if isinstance(val, object):
944 1
                try:
945 1
                    return getattr(VariantType, val.__class__.__name__)
946 1
                except AttributeError:
947 1
                    return VariantType.ExtensionObject
948
            else:
949
                raise UaError("Could not guess UA type of {} with type {}, specify UA type".format(val, type(val)))
950
951 1
    def __str__(self):
952 1
        return "Variant(val:{!s},type:{})".format(self.Value, self.VariantType)
953 1
    __repr__ = __str__
954
955 1
    def to_binary(self):
956 1
        b = []
957 1
        encoding = self.VariantType.value & 0b111111
958 1
        if type(self.Value) in (list, tuple):
959 1
            if self.Dimensions is not None:
960 1
                encoding = set_bit(encoding, 6)
961 1
            encoding = set_bit(encoding, 7)
962 1
            b.append(uatype_UInt8.pack(encoding))
963 1
            b.append(pack_uatype_array(self.VariantType.name, flatten(self.Value)))
964 1
            if self.Dimensions is not None:
965 1
                b.append(pack_uatype_array("Int32", self.Dimensions))
966
        else:
967 1
            b.append(uatype_UInt8.pack(encoding))
968 1
            b.append(pack_uatype(self.VariantType.name, self.Value))
969
970 1
        return b"".join(b)
971
972 1
    @staticmethod
973
    def from_binary(data):
974 1
        dimensions = None
975 1
        encoding = ord(data.read(1))
976 1
        int_type = encoding & 0b00111111
977 1
        if int_type > 25:
978 1
            vtype = VariantTypeCustom(int_type)
979
        else:
980 1
            vtype = VariantType(int_type)
981 1
        if vtype == VariantType.Null:
982 1
            return Variant(None, vtype, encoding)
983 1
        if test_bit(encoding, 7):
984 1
            value = unpack_uatype_array(vtype.name, data)
985
        else:
986 1
            value = unpack_uatype(vtype.name, data)
987 1
        if test_bit(encoding, 6):
988 1
            dimensions = unpack_uatype_array("Int32", data)
989 1
            value = reshape(value, dimensions)
990
991 1
        return Variant(value, vtype, dimensions)
992
993
994 1
def reshape(flat, dims):
995 1
    subdims = dims[1:]
996 1
    subsize = 1
997 1
    for i in subdims:
998 1
        if i == 0:
999 1
            i = 1
1000 1
        subsize *= i
1001 1
    while dims[0] * subsize > len(flat):
1002 1
        flat.append([])
1003 1
    if not subdims or subdims == [0]:
1004 1
        return flat
1005 1
    return [reshape(flat[i: i + subsize], subdims) for i in range(0, len(flat), subsize)]
1006
1007
1008 1
def _split_list(l, n):
1009
    n = max(1, n)
1010
    return [l[i:i + n] for i in range(0, len(l), n)]
1011
1012
1013 1
def flatten_and_get_shape(mylist):
1014
    dims = []
1015
    dims.append(len(mylist))
1016
    while isinstance(mylist[0], (list, tuple)):
1017
        dims.append(len(mylist[0]))
1018
        mylist = [item for sublist in mylist for item in sublist]
1019
        if len(mylist) == 0:
1020
            break
1021
    return mylist, dims
1022
1023
1024 1
def flatten(mylist):
1025 1
    if len(mylist) == 0:
1026 1
        return mylist
1027 1
    while isinstance(mylist[0], (list, tuple)):
1028 1
        mylist = [item for sublist in mylist for item in sublist]
1029 1
        if len(mylist) == 0:
1030 1
            break
1031 1
    return mylist
1032
1033
1034 1
def get_shape(mylist):
1035 1
    dims = []
1036 1
    while isinstance(mylist, (list, tuple)):
1037 1
        dims.append(len(mylist))
1038 1
        if len(mylist) == 0:
1039 1
            break
1040 1
        mylist = mylist[0]
1041 1
    return dims
1042
1043
1044 1
class XmlElement(FrozenClass):
1045
    '''
1046
    An XML element encoded as an UTF-8 string.
1047
    '''
1048 1
    def __init__(self, binary=None):
1049
        if binary is not None:
1050
            self._binary_init(binary)
1051
            self._freeze = True
1052
            return
1053
        self.Value = []
1054
        self._freeze = True
1055
1056 1
    def to_binary(self):
1057
        return pack_string(self.Value)
1058
1059 1
    @staticmethod
1060
    def from_binary(data):
1061
        return XmlElement(data)
1062
1063 1
    def _binary_init(self, data):
1064
        self.Value = unpack_string(data)
1065
1066 1
    def __str__(self):
1067
        return 'XmlElement(Value:' + str(self.Value) + ')'
1068
1069 1
    __repr__ = __str__
1070
1071
1072
1073
1074 1
class DataValue(FrozenClass):
1075
1076
    '''
1077
    A value with an associated timestamp, and quality.
1078
    Automatically generated from xml , copied and modified here to fix errors in xml spec
1079
1080
    :ivar Value:
1081
    :vartype Value: Variant
1082
    :ivar StatusCode:
1083
    :vartype StatusCode: StatusCode
1084
    :ivar SourceTimestamp:
1085
    :vartype SourceTimestamp: datetime
1086
    :ivar SourcePicoSeconds:
1087
    :vartype SourcePicoSeconds: int
1088
    :ivar ServerTimestamp:
1089
    :vartype ServerTimestamp: datetime
1090
    :ivar ServerPicoseconds:
1091
    :vartype ServerPicoseconds: int
1092
1093
    '''
1094
1095 1
    def __init__(self, variant=None, status=None):
1096 1
        self.Encoding = 0
1097 1
        if not isinstance(variant, Variant):
1098 1
            variant = Variant(variant)
1099 1
        self.Value = variant
1100 1
        if status is None:
1101 1
            self.StatusCode = StatusCode()
1102
        else:
1103 1
            self.StatusCode = status
1104 1
        self.SourceTimestamp = None  # DateTime()
1105 1
        self.SourcePicoseconds = None
1106 1
        self.ServerTimestamp = None  # DateTime()
1107 1
        self.ServerPicoseconds = None
1108 1
        self._freeze = True
1109
1110 1
    def to_binary(self):
1111 1
        packet = []
1112 1
        if self.Value:
1113 1
            self.Encoding |= (1 << 0)
1114 1
        if self.StatusCode:
1115 1
            self.Encoding |= (1 << 1)
1116 1
        if self.SourceTimestamp:
1117 1
            self.Encoding |= (1 << 2)
1118 1
        if self.ServerTimestamp:
1119 1
            self.Encoding |= (1 << 3)
1120 1
        if self.SourcePicoseconds:
1121
            self.Encoding |= (1 << 4)
1122 1
        if self.ServerPicoseconds:
1123
            self.Encoding |= (1 << 5)
1124 1
        packet.append(uatype_UInt8.pack(self.Encoding))
1125 1
        if self.Value:
1126 1
            packet.append(self.Value.to_binary())
1127 1
        if self.StatusCode:
1128 1
            packet.append(self.StatusCode.to_binary())
1129 1
        if self.SourceTimestamp:
1130 1
            packet.append(pack_datetime(self.SourceTimestamp))  # self.SourceTimestamp.to_binary())
1131 1
        if self.ServerTimestamp:
1132 1
            packet.append(pack_datetime(self.ServerTimestamp))  # self.ServerTimestamp.to_binary())
1133 1
        if self.SourcePicoseconds:
1134
            packet.append(uatype_UInt16.pack(self.SourcePicoseconds))
1135 1
        if self.ServerPicoseconds:
1136
            packet.append(uatype_UInt16.pack(self.ServerPicoseconds))
1137 1
        return b''.join(packet)
1138
1139 1
    @staticmethod
1140
    def from_binary(data):
1141 1
        encoding = ord(data.read(1))
1142 1
        if encoding & (1 << 0):
1143 1
            value = Variant.from_binary(data)
1144
        else:
1145
            value = None
1146 1
        if encoding & (1 << 1):
1147 1
            status = StatusCode.from_binary(data)
1148
        else:
1149
            status = None
1150 1
        obj = DataValue(value, status)
1151 1
        obj.Encoding = encoding
1152 1
        if obj.Encoding & (1 << 2):
1153 1
            obj.SourceTimestamp = unpack_datetime(data)  # DateTime.from_binary(data)
1154 1
        if obj.Encoding & (1 << 3):
1155 1
            obj.ServerTimestamp = unpack_datetime(data)  # DateTime.from_binary(data)
1156 1
        if obj.Encoding & (1 << 4):
1157
            obj.SourcePicoseconds = uatype_UInt16.unpack(data.read(2))[0]
1158 1
        if obj.Encoding & (1 << 5):
1159
            obj.ServerPicoseconds = uatype_UInt16.unpack(data.read(2))[0]
1160 1
        return obj
1161
1162 1
    def __str__(self):
1163
        s = 'DataValue(Value:{}'.format(self.Value)
1164
        if self.StatusCode is not None:
1165
            s += ', StatusCode:{}'.format(self.StatusCode)
1166
        if self.SourceTimestamp is not None:
1167
            s += ', SourceTimestamp:{}'.format(self.SourceTimestamp)
1168
        if self.ServerTimestamp is not None:
1169
            s += ', ServerTimestamp:{}'.format(self.ServerTimestamp)
1170
        if self.SourcePicoseconds is not None:
1171
            s += ', SourcePicoseconds:{}'.format(self.SourcePicoseconds)
1172
        if self.ServerPicoseconds is not None:
1173
            s += ', ServerPicoseconds:{}'.format(self.ServerPicoseconds)
1174
        s += ')'
1175
        return s
1176
1177 1
    __repr__ = __str__
1178
1179
1180
1181