Completed
Pull Request — master (#353)
by
unknown
05:25
created

LazyLoadingDict.__len__()   A

Complexity

Conditions 1

Size

Total Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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