Completed
Pull Request — master (#494)
by Olivier
03:37
created

AttributeService.read()   A

Complexity

Conditions 2

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 4.3145

Importance

Changes 0
Metric Value
cc 2
c 0
b 0
f 0
dl 0
loc 6
ccs 1
cts 6
cp 0.1666
crap 4.3145
rs 9.4285
1 1
from threading import RLock
2 1
import logging
3 1
from datetime import datetime
4 1
import collections
5 1
import shelve
6 1
try:
7 1
    import cPickle as pickle
8
except:
9
    import pickle
10
11 1
from opcua import ua
12 1
from opcua.server.users import User
13
14
15 1
class AttributeValue(object):
16
17 1
    def __init__(self, value):
18
        self.value = value
19
        self.value_callback = None
20
        self.datachange_callbacks = {}
21
22 1
    def __str__(self):
23
        return "AttributeValue({0})".format(self.value)
24 1
    __repr__ = __str__
25
26
27 1
class NodeData(object):
28
29 1
    def __init__(self, nodeid):
30
        self.nodeid = nodeid
31
        self.attributes = {}
32
        self.references = []
33
        self.call = None
34
35 1
    def __str__(self):
36
        return "NodeData(id:{0}, attrs:{1}, refs:{2})".format(self.nodeid, self.attributes, self.references)
37 1
    __repr__ = __str__
38
39
40 1
class AttributeService(object):
41
42 1
    def __init__(self, aspace):
43
        self.logger = logging.getLogger(__name__)
44
        self._aspace = aspace
45
46 1
    def read(self, params):
47
        self.logger.debug("read %s", params)
48
        res = []
49
        for readvalue in params.NodesToRead:
50
            res.append(self._aspace.get_attribute_value(readvalue.NodeId, readvalue.AttributeId))
51
        return res
52
53 1
    def write(self, params, user=User.Admin):
54
        self.logger.debug("write %s as user %s", params, user)
55
        res = []
56
        for writevalue in params.NodesToWrite:
57
            if user != User.Admin:
58
                if writevalue.AttributeId != ua.AttributeIds.Value:
59
                    res.append(ua.StatusCode(ua.StatusCodes.BadUserAccessDenied))
60
                    continue
61
                al = self._aspace.get_attribute_value(writevalue.NodeId, ua.AttributeIds.AccessLevel)
62
                ual = self._aspace.get_attribute_value(writevalue.NodeId, ua.AttributeIds.UserAccessLevel)
63
                if not ua.ua_binary.test_bit(al.Value.Value, ua.AccessLevel.CurrentWrite) or not ua.ua_binary.test_bit(ual.Value.Value, ua.AccessLevel.CurrentWrite):
64
                    res.append(ua.StatusCode(ua.StatusCodes.BadUserAccessDenied))
65
                    continue
66
            res.append(self._aspace.set_attribute_value(writevalue.NodeId, writevalue.AttributeId, writevalue.Value))
67
        return res
68
69
70 1
class ViewService(object):
71
72 1
    def __init__(self, aspace):
73
        self.logger = logging.getLogger(__name__)
74
        self._aspace = aspace
75
76 1
    def browse(self, params):
77
        self.logger.debug("browse %s", params)
78
        res = []
79
        for desc in params.NodesToBrowse:
80
            res.append(self._browse(desc))
81
        return res
82
83 1
    def _browse(self, desc):
84
        res = ua.BrowseResult()
85
        if desc.NodeId not in self._aspace:
86
            res.StatusCode = ua.StatusCode(ua.StatusCodes.BadNodeIdInvalid)
87
            return res
88
        node = self._aspace[desc.NodeId]
89
        for ref in node.references:
90
            if not self._is_suitable_ref(desc, ref):
91
                continue
92
            res.References.append(ref)
93
        return res
94
95 1
    def _is_suitable_ref(self, desc, ref):
96
        if not self._suitable_direction(desc.BrowseDirection, ref.IsForward):
97
            self.logger.debug("%s is not suitable due to direction", ref)
98
            return False
99
        if not self._suitable_reftype(desc.ReferenceTypeId, ref.ReferenceTypeId, desc.IncludeSubtypes):
100
            self.logger.debug("%s is not suitable due to type", ref)
101
            return False
102
        if desc.NodeClassMask and ((desc.NodeClassMask & ref.NodeClass) == 0):
103
            self.logger.debug("%s is not suitable due to class", ref)
104
            return False
105
        self.logger.debug("%s is a suitable ref for desc %s", ref, desc)
106
        return True
107
108 1
    def _suitable_reftype(self, ref1, ref2, subtypes):
109
        """
110
        """
111
        if ref1 == ua.NodeId(ua.ObjectIds.Null):
112
            # If ReferenceTypeId is not specified in the BrowseDescription,
113
            # all References are returned and includeSubtypes is ignored.
114
            return True
115
        if not subtypes and ref2.Identifier == ua.ObjectIds.HasSubtype:
116
            return False
117
        if ref1.Identifier == ref2.Identifier:
118
            return True
119
        oktypes = self._get_sub_ref(ref1)
120
        if not subtypes and ua.NodeId(ua.ObjectIds.HasSubtype) in oktypes:
121
            oktypes.remove(ua.NodeId(ua.ObjectIds.HasSubtype))
122
        return ref2 in oktypes
123
124 1
    def _get_sub_ref(self, ref):
125
        res = []
126
        nodedata = self._aspace[ref]
127
        if nodedata is not None:
128
            for ref in nodedata.references:
129
                if ref.ReferenceTypeId.Identifier == ua.ObjectIds.HasSubtype and ref.IsForward:
130
                    res.append(ref.NodeId)
131
                    res += self._get_sub_ref(ref.NodeId)
132
        return res
133
134 1
    def _suitable_direction(self, desc, isforward):
135
        if desc == ua.BrowseDirection.Both:
136
            return True
137
        if desc == ua.BrowseDirection.Forward and isforward:
138
            return True
139
        if desc == ua.BrowseDirection.Inverse and not isforward:
140
            return True
141
        return False
142
143 1
    def translate_browsepaths_to_nodeids(self, browsepaths):
144
        self.logger.debug("translate browsepath: %s", browsepaths)
145
        results = []
146
        for path in browsepaths:
147
            results.append(self._translate_browsepath_to_nodeid(path))
148
        return results
149
150 1
    def _translate_browsepath_to_nodeid(self, path):
151
        self.logger.debug("looking at path: %s", path)
152
        res = ua.BrowsePathResult()
153
        if path.StartingNode not in self._aspace:
154
            res.StatusCode = ua.StatusCode(ua.StatusCodes.BadNodeIdInvalid)
155
            return res
156
        current = path.StartingNode
157
        for el in path.RelativePath.Elements:
158
            nodeid = self._find_element_in_node(el, current)
159
            if not nodeid:
160
                res.StatusCode = ua.StatusCode(ua.StatusCodes.BadNoMatch)
161
                return res
162
            current = nodeid
163
        target = ua.BrowsePathTarget()
164
        target.TargetId = current
165
        target.RemainingPathIndex = 4294967295
166
        res.Targets = [target]
167
        return res
168
169 1
    def _find_element_in_node(self, el, nodeid):
170
        nodedata = self._aspace[nodeid]
171
        for ref in nodedata.references:
172
            # FIXME: here we should check other arguments!!
173
            if ref.BrowseName == el.TargetName:
174
                return ref.NodeId
175
        self.logger.info("element %s was not found in node %s", el, nodeid)
176
        return None
177
178
179 1
class NodeManagementService(object):
180
181 1
    def __init__(self, aspace):
182
        self.logger = logging.getLogger(__name__)
183
        self._aspace = aspace
184
185 1
    def add_nodes(self, addnodeitems, user=User.Admin):
186
        results = []
187
        for item in addnodeitems:
188
            results.append(self._add_node(item, user))
189
        return results
190
191 1
    def _add_node(self, item, user):
192
        result = ua.AddNodesResult()
193
194
        # If Identifier of requested NodeId is null we generate a new NodeId using
195
        # the namespace of the nodeid, this is an extention of the spec to allow
196
        # to requests the server to generate a new nodeid in a specified namespace
197
        if item.RequestedNewNodeId.has_null_identifier():
198
            self.logger.debug("RequestedNewNodeId has null identifier, generating Identifier")
199
            nodedata = NodeData(self._aspace.generate_nodeid(item.RequestedNewNodeId.NamespaceIndex))
200
        else:
201
            nodedata = NodeData(item.RequestedNewNodeId)
202
203
        if nodedata.nodeid in self._aspace:
204
            self.logger.warning("AddNodesItem: Requested NodeId %s already exists", nodedata.nodeid)
205
            result.StatusCode = ua.StatusCode(ua.StatusCodes.BadNodeIdExists)
206
            return result
207
208
        if item.ParentNodeId.is_null():
209
            # self.logger.warning("add_node: creating node %s without parent", nodedata.nodeid)
210
            # should return Error here, but the standard namespace define many nodes without parents...
211
            pass
212
        elif item.ParentNodeId not in self._aspace:
213
            self.logger.warning("add_node: while adding node %s, requested parent node %s does not exists", nodedata.nodeid, item.ParentNodeId)
214
            result.StatusCode = ua.StatusCode(ua.StatusCodes.BadParentNodeIdInvalid)
215
            return result
216
217
        if not user == User.Admin:
218
            result.StatusCode = ua.StatusCode(ua.StatusCodes.BadUserAccessDenied)
219
            return result
220
221
        self._add_node_attributes(nodedata, item)
222
223
        # now add our node to db
224
        self._aspace[nodedata.nodeid] = nodedata
225
226
        if not item.ParentNodeId.is_null():
227
            self._add_ref_from_parent(nodedata, item)
228
            self._add_ref_to_parent(nodedata, item, user)
229
230
        # add type definition
231
        if item.TypeDefinition != ua.NodeId():
232
            self._add_type_definition(nodedata, item, user)
233
234
        result.StatusCode = ua.StatusCode()
235
        result.AddedNodeId = nodedata.nodeid
236
237
        return result
238
239 1
    def _add_node_attributes(self, nodedata, item):
240
        # add common attrs
241
        nodedata.attributes[ua.AttributeIds.NodeId] = AttributeValue(
242
            ua.DataValue(ua.Variant(nodedata.nodeid, ua.VariantType.NodeId))
243
        )
244
        nodedata.attributes[ua.AttributeIds.BrowseName] = AttributeValue(
245
            ua.DataValue(ua.Variant(item.BrowseName, ua.VariantType.QualifiedName))
246
        )
247
        nodedata.attributes[ua.AttributeIds.NodeClass] = AttributeValue(
248
            ua.DataValue(ua.Variant(item.NodeClass, ua.VariantType.Int32))
249
        )
250
        # add requested attrs
251
        self._add_nodeattributes(item.NodeAttributes, nodedata)
252
253 1
    def _add_ref_from_parent(self, nodedata, item):
254
        desc = ua.ReferenceDescription()
255
        desc.ReferenceTypeId = item.ReferenceTypeId
256
        desc.NodeId = nodedata.nodeid
257
        desc.NodeClass = item.NodeClass
258
        desc.BrowseName = item.BrowseName
259
        desc.DisplayName = item.NodeAttributes.DisplayName
260
        desc.TypeDefinition = item.TypeDefinition
261
        desc.IsForward = True
262
        self._aspace[item.ParentNodeId].references.append(desc)
263
264 1
    def _add_ref_to_parent(self, nodedata, item, user):
265
        addref = ua.AddReferencesItem()
266
        addref.ReferenceTypeId = item.ReferenceTypeId
267
        addref.SourceNodeId = nodedata.nodeid
268
        addref.TargetNodeId = item.ParentNodeId
269
        addref.TargetNodeClass = self._aspace[item.ParentNodeId].attributes[ua.AttributeIds.NodeClass].value.Value.Value
270
        addref.IsForward = False
271
        self._add_reference(addref, user)
272
273 1
    def _add_type_definition(self, nodedata, item, user):
274
        addref = ua.AddReferencesItem()
275
        addref.SourceNodeId = nodedata.nodeid
276
        addref.IsForward = True
277
        addref.ReferenceTypeId = ua.NodeId(ua.ObjectIds.HasTypeDefinition)
278
        addref.TargetNodeId = item.TypeDefinition
279
        addref.TargetNodeClass = ua.NodeClass.DataType
280
        self._add_reference(addref, user)
281
282 1
    def delete_nodes(self, deletenodeitems, user=User.Admin):
283
        results = []
284
        for item in deletenodeitems.NodesToDelete:
285
            results.append(self._delete_node(item, user))
286
        return results
287
288 1
    def _delete_node(self, item, user):
289
        if user != User.Admin:
290
            return ua.StatusCode(ua.StatusCodes.BadUserAccessDenied)
291
292
        if item.NodeId not in self._aspace:
293
            self.logger.warning("DeleteNodesItem: NodeId %s does not exists", item.NodeId)
294
            return ua.StatusCode(ua.StatusCodes.BadNodeIdUnknown)
295
296
        if item.DeleteTargetReferences:
297
            for elem in self._aspace.keys():
298
                for rdesc in self._aspace[elem].references:
299
                    if rdesc.NodeId == item.NodeId:
300
                        self._aspace[elem].references.remove(rdesc)
301
302
        self._delete_node_callbacks(self._aspace[item.NodeId])
303
304
        del(self._aspace[item.NodeId])
305
306
        return ua.StatusCode()
307
308 1
    def _delete_node_callbacks(self, nodedata):
309
        if ua.AttributeIds.Value in nodedata.attributes:
310
            for handle, callback in nodedata.attributes[ua.AttributeIds.Value].datachange_callbacks.items():
311
                try:
312
                    callback(handle, None, ua.StatusCode(ua.StatusCodes.BadNodeIdUnknown))
313
                    self._aspace.delete_datachange_callback(handle)
314
                except Exception as ex:
315
                    self.logger.exception("Error calling delete node callback callback %s, %s, %s", nodedata, ua.AttributeIds.Value, ex)
316
317 1
    def add_references(self, refs, user=User.Admin):
318
        result = []
319
        for ref in refs:
320
            result.append(self._add_reference(ref, user))
321
        return result
322
323 1
    def _add_reference(self, addref, user):
324
        if addref.SourceNodeId not in self._aspace:
325
            return ua.StatusCode(ua.StatusCodes.BadSourceNodeIdInvalid)
326
        if addref.TargetNodeId not in self._aspace:
327
            return ua.StatusCode(ua.StatusCodes.BadTargetNodeIdInvalid)
328
        if user != User.Admin:
329
            return ua.StatusCode(ua.StatusCodes.BadUserAccessDenied)
330
        rdesc = ua.ReferenceDescription()
331
        rdesc.ReferenceTypeId = addref.ReferenceTypeId
332
        rdesc.IsForward = addref.IsForward
333
        rdesc.NodeId = addref.TargetNodeId
334
        rdesc.NodeClass = addref.TargetNodeClass
335
        bname = self._aspace.get_attribute_value(addref.TargetNodeId, ua.AttributeIds.BrowseName).Value.Value
336
        if bname:
337
            rdesc.BrowseName = bname
338
        dname = self._aspace.get_attribute_value(addref.TargetNodeId, ua.AttributeIds.DisplayName).Value.Value
339
        if dname:
340
            rdesc.DisplayName = dname
341
        self._aspace[addref.SourceNodeId].references.append(rdesc)
342
        return ua.StatusCode()
343
344 1
    def delete_references(self, refs, user=User.Admin):
345
        result = []
346
        for ref in refs:
347
            result.append(self._delete_reference(ref, user))
348
        return result
349
350 1
    def _delete_reference(self, item, user):
351
        if item.SourceNodeId not in self._aspace:
352
            return ua.StatusCode(ua.StatusCodes.BadSourceNodeIdInvalid)
353
        if item.TargetNodeId not in self._aspace:
354
            return ua.StatusCode(ua.StatusCodes.BadTargetNodeIdInvalid)
355
        if user != User.Admin:
356
            return ua.StatusCode(ua.StatusCodes.BadUserAccessDenied)
357
358
        for rdesc in self._aspace[item.SourceNodeId].references:
359
            if rdesc.NodeId is item.TargetNodeId:
360
                if rdesc.ReferenceTypeId != item.ReferenceTypeId:
361
                    return ua.StatusCode(ua.StatusCodes.BadReferenceTypeIdInvalid)
362
                if rdesc.IsForward == item.IsForward or item.DeleteBidirectional:
363
                    self._aspace[item.SourceNodeId].references.remove(rdesc)
364
365
        for rdesc in self._aspace[item.TargetNodeId].references:
366
            if rdesc.NodeId is item.SourceNodeId:
367
                if rdesc.ReferenceTypeId != item.ReferenceTypeId:
368
                    return ua.StatusCode(ua.StatusCodes.BadReferenceTypeIdInvalid)
369
                if rdesc.IsForward == item.IsForward or item.DeleteBidirectional:
370
                    self._aspace[item.SourceNodeId].references.remove(rdesc)
371
372
        return ua.StatusCode()
373
374 1
    def _add_node_attr(self, item, nodedata, name, vtype=None):
375
        if item.SpecifiedAttributes & getattr(ua.NodeAttributesMask, name):
376
            dv = ua.DataValue(ua.Variant(getattr(item, name), vtype))
377
            dv.ServerTimestamp = datetime.utcnow()
378
            dv.SourceTimestamp = datetime.utcnow()
379
            nodedata.attributes[getattr(ua.AttributeIds, name)] = AttributeValue(dv)
380
381 1
    def _add_nodeattributes(self, item, nodedata):
382
        self._add_node_attr(item, nodedata, "AccessLevel", ua.VariantType.Byte)
383
        self._add_node_attr(item, nodedata, "ArrayDimensions", ua.VariantType.UInt32)
384
        self._add_node_attr(item, nodedata, "BrowseName", ua.VariantType.QualifiedName)
385
        self._add_node_attr(item, nodedata, "ContainsNoLoops", ua.VariantType.Boolean)
386
        self._add_node_attr(item, nodedata, "DataType", ua.VariantType.NodeId)
387
        self._add_node_attr(item, nodedata, "Description", ua.VariantType.LocalizedText)
388
        self._add_node_attr(item, nodedata, "DisplayName", ua.VariantType.LocalizedText)
389
        self._add_node_attr(item, nodedata, "EventNotifier", ua.VariantType.Byte)
390
        self._add_node_attr(item, nodedata, "Executable", ua.VariantType.Boolean)
391
        self._add_node_attr(item, nodedata, "Historizing", ua.VariantType.Boolean)
392
        self._add_node_attr(item, nodedata, "InverseName", ua.VariantType.LocalizedText)
393
        self._add_node_attr(item, nodedata, "IsAbstract", ua.VariantType.Boolean)
394
        self._add_node_attr(item, nodedata, "MinimumSamplingInterval", ua.VariantType.Double)
395
        self._add_node_attr(item, nodedata, "NodeClass", ua.VariantType.UInt32)
396
        self._add_node_attr(item, nodedata, "NodeId", ua.VariantType.NodeId)
397
        self._add_node_attr(item, nodedata, "Symmetric", ua.VariantType.Boolean)
398
        self._add_node_attr(item, nodedata, "UserAccessLevel", ua.VariantType.Byte)
399
        self._add_node_attr(item, nodedata, "UserExecutable", ua.VariantType.Boolean)
400
        self._add_node_attr(item, nodedata, "UserWriteMask", ua.VariantType.Byte)
401
        self._add_node_attr(item, nodedata, "ValueRank", ua.VariantType.Int32)
402
        self._add_node_attr(item, nodedata, "WriteMask", ua.VariantType.UInt32)
403
        self._add_node_attr(item, nodedata, "UserWriteMask", ua.VariantType.UInt32)
404
        self._add_node_attr(item, nodedata, "Value")
405
406
407 1
class MethodService(object):
408
409 1
    def __init__(self, aspace):
410
        self.logger = logging.getLogger(__name__)
411
        self._aspace = aspace
412
413 1
    def call(self, methods):
414
        results = []
415
        for method in methods:
416
            results.append(self._call(method))
417
        return results
418
419 1
    def _call(self, method):
420
        res = ua.CallMethodResult()
421
        if method.ObjectId not in self._aspace or method.MethodId not in self._aspace:
422
            res.StatusCode = ua.StatusCode(ua.StatusCodes.BadNodeIdInvalid)
423
        else:
424
            node = self._aspace[method.MethodId]
425
            if node.call is None:
426
                res.StatusCode = ua.StatusCode(ua.StatusCodes.BadNothingToDo)
427
            else:
428
                try:
429
                    res.OutputArguments = node.call(method.ObjectId, *method.InputArguments)
430
                    for _ in method.InputArguments:
431
                        res.InputArgumentResults.append(ua.StatusCode())
432
                except Exception:
433
                    self.logger.exception("Error executing method call %s, an exception was raised: ", method)
434
                    res.StatusCode = ua.StatusCode(ua.StatusCodes.BadUnexpectedError)
435
        return res
436
437
438 1
class AddressSpace(object):
439
440
    """
441
    The address space object stores all the nodes of the OPC-UA server
442
    and helper methods.
443
    The methods are thread safe
444
    """
445
446 1
    def __init__(self):
447
        self.logger = logging.getLogger(__name__)
448
        self._nodes = {}
449
        self._lock = RLock()  # FIXME: should use multiple reader, one writter pattern
450
        self._datachange_callback_counter = 200
451
        self._handle_to_attribute_map = {}
452
        self._default_idx = 2
453
        self._nodeid_counter = {0: 20000, 1: 2000}
454
455 1
    def __getitem__(self, nodeid):
456
        with self._lock:
457
            if nodeid in self._nodes:
458
                return self._nodes.__getitem__(nodeid)
459
460 1
    def __setitem__(self, nodeid, value):
461
        with self._lock:
462
            return self._nodes.__setitem__(nodeid, value)
463
464 1
    def __contains__(self, nodeid):
465
        with self._lock:
466
            return self._nodes.__contains__(nodeid)
467
468 1
    def __delitem__(self, nodeid):
469
        with self._lock:
470
            self._nodes.__delitem__(nodeid)
471
472 1
    def generate_nodeid(self, idx=None):
473
        if idx is None:
474
            idx = self._default_idx
475
        if idx in self._nodeid_counter:
476
            self._nodeid_counter[idx] += 1
477
        else:
478
            self._nodeid_counter[idx] = 1
479
        nodeid = ua.NodeId(self._nodeid_counter[idx], idx)
480
        with self._lock:  # OK since reentrant lock
481
            while True:
482
                if nodeid in self._nodes:
483
                    nodeid = self.generate_nodeid(idx)
484
                else:
485
                    return nodeid
486
487 1
    def keys(self):
488
        with self._lock:
489
            return self._nodes.keys()
490
491 1
    def empty(self):
492
        """
493
        Delete all nodes in address space
494
        """
495
        with self._lock:
496
            self._nodes = {}
497
498 1
    def dump(self, path):
499
        """
500
        Dump address space as binary to file; note that server must be stopped for this method to work
501
        DO NOT DUMP AN ADDRESS SPACE WHICH IS USING A SHELF (load_aspace_shelf), ONLY CACHED NODES WILL GET DUMPED!
502
        """
503
        # prepare nodes in address space for being serialized
504
        for nodeid, ndata in self._nodes.items():
505
            # if the node has a reference to a method call, remove it so the object can be serialized
506
            if ndata.call is not None:
507
                self._nodes[nodeid].call = None
508
509
        with open(path, 'wb') as f:
510
            pickle.dump(self._nodes, f, pickle.HIGHEST_PROTOCOL)
511
512 1
    def load(self, path):
513
        """
514
        Load address space from a binary file, overwriting everything in the current address space
515
        """
516
        with open(path, 'rb') as f:
517
            self._nodes = pickle.load(f)
518
519 1
    def make_aspace_shelf(self, path):
520
        """
521
        Make a shelf for containing the nodes from the standard address space; this is typically only done on first
522
        start of the server. Subsequent server starts will load the shelf, nodes are then moved to a cache
523
        by the LazyLoadingDict class when they are accessed. Saving data back to the shelf
524
        is currently NOT supported, it is only used for the default OPC UA standard address space
525
526
        Note: Intended for slow devices, such as Raspberry Pi, to greatly improve start up time
527
        """
528
        s = shelve.open(path, "n", protocol=pickle.HIGHEST_PROTOCOL)
529
        for nodeid, ndata in self._nodes.items():
530
            s[nodeid.to_string()] = ndata
531
        s.close()
532
533 1
    def load_aspace_shelf(self, path):
534
        """
535
        Load the standard address space nodes from a python shelve via LazyLoadingDict as needed.
536
        The dump() method can no longer be used if the address space is being loaded from a shelf
537
538
        Note: Intended for slow devices, such as Raspberry Pi, to greatly improve start up time
539
        """
540
        class LazyLoadingDict(collections.MutableMapping):
541
            """
542
            Special dict that only loads nodes as they are accessed. If a node is accessed it gets copied from the
543
            shelve to the cache dict. All user nodes are saved in the cache ONLY. Saving data back to the shelf
544
            is currently NOT supported
545
            """
546
            def __init__(self, source):
547
                self.source = source  # python shelf
548
                self.cache = {}  # internal dict
549
550
            def __getitem__(self, key):
551
                # try to get the item (node) from the cache, if it isn't there get it from the shelf
552
                try:
553
                    return self.cache[key]
554
                except KeyError:
555
                    node = self.cache[key] = self.source[key.to_string()]
556
                    return node
557
558
            def __setitem__(self, key, value):
559
                # add a new item to the cache; if this item is in the shelf it is not updated
560
                self.cache[key] = value
561
562
            def __contains__(self, key):
563
                return key in self.cache or key.to_string() in self.source
564
565
            def __delitem__(self, key):
566
                # only deleting items from the cache is allowed
567
                del self.cache[key]
568
569
            def __iter__(self):
570
                # only the cache can be iterated over
571
                return iter(self.cache.keys())
572
573
            def __len__(self):
574
                # only returns the length of items in the cache, not unaccessed items in the shelf
575
                return len(self.cache)
576
577
        self._nodes = LazyLoadingDict(shelve.open(path, "r"))
578
579 1
    def get_attribute_value(self, nodeid, attr):
580
        with self._lock:
581
            self.logger.debug("get attr val: %s %s", nodeid, attr)
582
            if nodeid not in self._nodes:
583
                dv = ua.DataValue()
584
                dv.StatusCode = ua.StatusCode(ua.StatusCodes.BadNodeIdUnknown)
585
                return dv
586
            node = self._nodes[nodeid]
587
            if attr not in node.attributes:
588
                dv = ua.DataValue()
589
                dv.StatusCode = ua.StatusCode(ua.StatusCodes.BadAttributeIdInvalid)
590
                return dv
591
            attval = node.attributes[attr]
592
            if attval.value_callback:
593
                return attval.value_callback()
594
            return attval.value
595
596 1
    def set_attribute_value(self, nodeid, attr, value):
597
        with self._lock:
598
            self.logger.debug("set attr val: %s %s %s", nodeid, attr, value)
599
            if nodeid not in self._nodes:
600
                return ua.StatusCode(ua.StatusCodes.BadNodeIdUnknown)
601
            node = self._nodes[nodeid]
602
            if attr not in node.attributes:
603
                return ua.StatusCode(ua.StatusCodes.BadAttributeIdInvalid)
604
            if not value.SourceTimestamp:
605
                value.SourceTimestamp = datetime.utcnow()
606
            if not value.ServerTimestamp:
607
                value.ServerTimestamp = datetime.utcnow()
608
609
            attval = node.attributes[attr]
610
            old = attval.value
611
            attval.value = value
612
            cbs = []
613
            if old.Value != value.Value:  # only send call callback when a value change has happend
614
                cbs = list(attval.datachange_callbacks.items())
615
616
        for k, v in cbs:
617
            try:
618
                v(k, value)
619
            except Exception as ex:
620
                self.logger.exception("Error calling datachange callback %s, %s, %s", k, v, ex)
621
622
        return ua.StatusCode()
623
624 1
    def add_datachange_callback(self, nodeid, attr, callback):
625
        with self._lock:
626
            self.logger.debug("set attr callback: %s %s %s", nodeid, attr, callback)
627
            if nodeid not in self._nodes:
628
                return ua.StatusCode(ua.StatusCodes.BadNodeIdUnknown), 0
629
            node = self._nodes[nodeid]
630
            if attr not in node.attributes:
631
                return ua.StatusCode(ua.StatusCodes.BadAttributeIdInvalid), 0
632
            attval = node.attributes[attr]
633
            self._datachange_callback_counter += 1
634
            handle = self._datachange_callback_counter
635
            attval.datachange_callbacks[handle] = callback
636
            self._handle_to_attribute_map[handle] = (nodeid, attr)
637
            return ua.StatusCode(), handle
638
639 1
    def delete_datachange_callback(self, handle):
640
        with self._lock:
641
            if handle in self._handle_to_attribute_map:
642
                nodeid, attr = self._handle_to_attribute_map.pop(handle)
643
                self._nodes[nodeid].attributes[attr].datachange_callbacks.pop(handle)
644
645 1
    def add_method_callback(self, methodid, callback):
646
        with self._lock:
647
            node = self._nodes[methodid]
648
            node.call = callback
649