Passed
Push — master ( 0b26fe...78dc7d )
by Olivier
04:21
created

LazyLoadingDict.__delitem__()   A

Complexity

Conditions 1

Size

Total Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
dl 0
loc 3
ccs 0
cts 2
cp 0
crap 2
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
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 1
        self.value = value
19 1
        self.value_callback = None
20 1
        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 1
        self.nodeid = nodeid
31 1
        self.attributes = {}
32 1
        self.references = []
33 1
        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 1
        self.logger = logging.getLogger(__name__)
44 1
        self._aspace = aspace
45
46 1
    def read(self, params):
47 1
        self.logger.debug("read %s", params)
48 1
        res = []
49 1
        for readvalue in params.NodesToRead:
50 1
            res.append(self._aspace.get_attribute_value(readvalue.NodeId, readvalue.AttributeId))
51 1
        return res
52
53 1
    def write(self, params, user=User.Admin):
54 1
        self.logger.debug("write %s as user %s", params, user)
55 1
        res = []
56 1
        for writevalue in params.NodesToWrite:
57 1
            if user != User.Admin:
58 1
                if writevalue.AttributeId != ua.AttributeIds.Value:
59 1
                    res.append(ua.StatusCode(ua.StatusCodes.BadUserAccessDenied))
60 1
                    continue
61 1
                al = self._aspace.get_attribute_value(writevalue.NodeId, ua.AttributeIds.AccessLevel)
62 1
                ual = self._aspace.get_attribute_value(writevalue.NodeId, ua.AttributeIds.UserAccessLevel)
63 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):
64 1
                    res.append(ua.StatusCode(ua.StatusCodes.BadUserAccessDenied))
65 1
                    continue
66 1
            res.append(self._aspace.set_attribute_value(writevalue.NodeId, writevalue.AttributeId, writevalue.Value))
67 1
        return res
68
69
70 1
class ViewService(object):
71
72 1
    def __init__(self, aspace):
73 1
        self.logger = logging.getLogger(__name__)
74 1
        self._aspace = aspace
75
76 1
    def browse(self, params):
77 1
        self.logger.debug("browse %s", params)
78 1
        res = []
79 1
        for desc in params.NodesToBrowse:
80 1
            res.append(self._browse(desc))
81 1
        return res
82
83 1
    def _browse(self, desc):
84 1
        res = ua.BrowseResult()
85 1
        if desc.NodeId not in self._aspace:
86 1
            res.StatusCode = ua.StatusCode(ua.StatusCodes.BadNodeIdInvalid)
87 1
            return res
88 1
        node = self._aspace[desc.NodeId]
89 1
        for ref in node.references:
90 1
            if not self._is_suitable_ref(desc, ref):
91 1
                continue
92 1
            res.References.append(ref)
93 1
        return res
94
95 1
    def _is_suitable_ref(self, desc, ref):
96 1
        if not self._suitable_direction(desc.BrowseDirection, ref.IsForward):
97 1
            self.logger.debug("%s is not suitable due to direction", ref)
98 1
            return False
99 1
        if not self._suitable_reftype(desc.ReferenceTypeId, ref.ReferenceTypeId, desc.IncludeSubtypes):
100 1
            self.logger.debug("%s is not suitable due to type", ref)
101 1
            return False
102 1
        if desc.NodeClassMask and ((desc.NodeClassMask & ref.NodeClass) == 0):
103 1
            self.logger.debug("%s is not suitable due to class", ref)
104 1
            return False
105 1
        self.logger.debug("%s is a suitable ref for desc %s", ref, desc)
106 1
        return True
107
108 1
    def _suitable_reftype(self, ref1, ref2, subtypes):
109
        """
110
        """
111 1
        if not subtypes and ref2.Identifier == ua.ObjectIds.HasSubtype:
112 1
            return False
113 1
        if ref1.Identifier == ref2.Identifier:
114 1
            return True
115 1
        oktypes = self._get_sub_ref(ref1)
116 1
        if not subtypes and ua.NodeId(ua.ObjectIds.HasSubtype) in oktypes:
117 1
            oktypes.remove(ua.NodeId(ua.ObjectIds.HasSubtype))
118 1
        return ref2 in oktypes
119
120 1
    def _get_sub_ref(self, ref):
121 1
        res = []
122 1
        nodedata = self._aspace[ref]
123 1
        if nodedata is not None:
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 1
                    res += self._get_sub_ref(ref.NodeId)
128 1
        return res
129
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 1
            return True
137 1
        return False
138
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 1
            results.append(self._translate_browsepath_to_nodeid(path))
144 1
        return results
145
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 1
        res.Targets = [target]
163 1
        return res
164
165 1
    def _find_element_in_node(self, el, nodeid):
166 1
        nodedata = self._aspace[nodeid]
167 1
        for ref in nodedata.references:
168
            # FIXME: here we should check other arguments!!
169 1
            if ref.BrowseName == el.TargetName:
170 1
                return ref.NodeId
171 1
        self.logger.info("element %s was not found in node %s", el, nodeid)
172 1
        return None
173
174
175 1
class NodeManagementService(object):
176
177 1
    def __init__(self, aspace):
178 1
        self.logger = logging.getLogger(__name__)
179 1
        self._aspace = aspace
180
181 1
    def add_nodes(self, addnodeitems, user=User.Admin):
182 1
        results = []
183 1
        for item in addnodeitems:
184 1
            results.append(self._add_node(item, user))
185 1
        return results
186
187 1
    def _add_node(self, item, user):
188 1
        result = ua.AddNodesResult()
189
190
        # If Identifier of requested NodeId is null we generate a new NodeId using
191
        # the namespace of the nodeid, this is an extention of the spec to allow
192
        # to requests the server to generate a new nodeid in a specified namespace
193 1
        if item.RequestedNewNodeId.has_null_identifier():
194 1
            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
199 1
        if nodedata.nodeid in self._aspace:
200 1
            self.logger.warning("AddNodesItem: Requested NodeId %s already exists", nodedata.nodeid)
201 1
            result.StatusCode = ua.StatusCode(ua.StatusCodes.BadNodeIdExists)
202 1
            return result
203
204 1
        if item.ParentNodeId.is_null():
205
            # self.logger.warning("add_node: creating node %s without parent", nodedata.nodeid)
206
            # 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 1
            result.StatusCode = ua.StatusCode(ua.StatusCodes.BadParentNodeIdInvalid)
211 1
            return result
212
213 1
        if not user == User.Admin:
214 1
            result.StatusCode = ua.StatusCode(ua.StatusCodes.BadUserAccessDenied)
215 1
            return result
216
217 1
        self._add_node_attributes(nodedata, item)
218
219
        # now add our node to db
220 1
        self._aspace[nodedata.nodeid] = nodedata
221
222 1
        if not item.ParentNodeId.is_null():
223 1
            self._add_ref_from_parent(nodedata, item)
224 1
            self._add_ref_to_parent(nodedata, item, user)
225
226
        # add type definition
227 1
        if item.TypeDefinition != ua.NodeId():
228 1
            self._add_type_definition(nodedata, item, user)
229
230 1
        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 1
        nodedata.attributes[ua.AttributeIds.NodeId] = AttributeValue(
238
            ua.DataValue(ua.Variant(nodedata.nodeid, ua.VariantType.NodeId))
239
        )
240 1
        nodedata.attributes[ua.AttributeIds.BrowseName] = AttributeValue(
241
            ua.DataValue(ua.Variant(item.BrowseName, ua.VariantType.QualifiedName))
242
        )
243 1
        nodedata.attributes[ua.AttributeIds.NodeClass] = AttributeValue(
244
            ua.DataValue(ua.Variant(item.NodeClass, ua.VariantType.Int32))
245
        )
246
        # add requested attrs
247 1
        self._add_nodeattributes(item.NodeAttributes, nodedata)
248
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 = item.NodeAttributes.DisplayName
256 1
        desc.TypeDefinition = item.TypeDefinition
257 1
        desc.IsForward = True
258 1
        self._aspace[item.ParentNodeId].references.append(desc)
259
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 1
        addref.IsForward = False
267 1
        self._add_reference(addref, user)
268
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 1
        addref.TargetNodeClass = ua.NodeClass.DataType
276 1
        self._add_reference(addref, user)
277
278 1
    def delete_nodes(self, deletenodeitems, user=User.Admin):
279 1
        results = []
280 1
        for item in deletenodeitems.NodesToDelete:
281 1
            results.append(self._delete_node(item, user))
282 1
        return results
283
284 1
    def _delete_node(self, item, user):
285 1
        if user != User.Admin:
286
            return ua.StatusCode(ua.StatusCodes.BadUserAccessDenied)
287
288 1
        if item.NodeId not in self._aspace:
289
            self.logger.warning("DeleteNodesItem: NodeId %s does not exists", item.NodeId)
290
            return ua.StatusCode(ua.StatusCodes.BadNodeIdUnknown)
291
292 1
        if item.DeleteTargetReferences:
293 1
            for elem in self._aspace.keys():
294 1
                for rdesc in self._aspace[elem].references:
295 1
                    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
304 1
    def _delete_node_callbacks(self, nodedata):
305 1
        if ua.AttributeIds.Value in nodedata.attributes:
306 1
            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
                    self.logger.exception("Error calling delete node callback callback %s, %s, %s", nodedata, ua.AttributeIds.Value, ex)
312
313 1
    def add_references(self, refs, user=User.Admin):
314 1
        result = []
315 1
        for ref in refs:
316 1
            result.append(self._add_reference(ref, user))
317 1
        return result
318
319 1
    def _add_reference(self, addref, user):
320 1
        if addref.SourceNodeId not in self._aspace:
321
            return ua.StatusCode(ua.StatusCodes.BadSourceNodeIdInvalid)
322 1
        if addref.TargetNodeId not in self._aspace:
323 1
            return ua.StatusCode(ua.StatusCodes.BadTargetNodeIdInvalid)
324 1
        if user != User.Admin:
325
            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 1
        self._aspace[addref.SourceNodeId].references.append(rdesc)
338 1
        return ua.StatusCode()
339
340 1
    def delete_references(self, refs, user=User.Admin):
341
        result = []
342
        for ref in refs:
343
            result.append(self._delete_reference(ref, user))
344
        return result
345
346 1
    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.ReferenceTypeId != item.ReferenceTypeId:
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.ReferenceTypeId != item.ReferenceTypeId:
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
        return ua.StatusCode()
369
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 1
            dv.SourceTimestamp = datetime.utcnow()
375 1
            nodedata.attributes[getattr(ua.AttributeIds, name)] = AttributeValue(dv)
376
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 1
        self._add_node_attr(item, nodedata, "UserWriteMask", ua.VariantType.UInt32)
400 1
        self._add_node_attr(item, nodedata, "Value")
401
402
403 1
class MethodService(object):
404
405 1
    def __init__(self, aspace):
406 1
        self.logger = logging.getLogger(__name__)
407 1
        self._aspace = aspace
408
409 1
    def call(self, methods):
410 1
        results = []
411 1
        for method in methods:
412 1
            results.append(self._call(method))
413 1
        return results
414
415 1
    def _call(self, method):
416 1
        res = ua.CallMethodResult()
417 1
        if method.ObjectId not in self._aspace or method.MethodId not in self._aspace:
418 1
            res.StatusCode = ua.StatusCode(ua.StatusCodes.BadNodeIdInvalid)
419
        else:
420 1
            node = self._aspace[method.MethodId]
421 1
            if node.call is None:
422
                res.StatusCode = ua.StatusCode(ua.StatusCodes.BadNothingToDo)
423
            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 1
                    res.StatusCode = ua.StatusCode(ua.StatusCodes.BadUnexpectedError)
431 1
        return res
432
433
434 1
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
    """
441
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 1
        self._default_idx = 2
449 1
        self._nodeid_counter = {0: 20000, 1: 2000}
450
451 1
    def __getitem__(self, nodeid):
452 1
        with self._lock:
453 1
            if nodeid in self._nodes:
454 1
                return self._nodes.__getitem__(nodeid)
455
456 1
    def __setitem__(self, nodeid, value):
457 1
        with self._lock:
458 1
            return self._nodes.__setitem__(nodeid, value)
459
460 1
    def __contains__(self, nodeid):
461 1
        with self._lock:
462 1
            return self._nodes.__contains__(nodeid)
463
464 1
    def __delitem__(self, nodeid):
465 1
        with self._lock:
466 1
            self._nodes.__delitem__(nodeid)
467
468 1
    def generate_nodeid(self, idx=None):
469 1
        if idx is None:
470
            idx = self._default_idx
471 1
        if idx in self._nodeid_counter:
472 1
            self._nodeid_counter[idx] += 1
473
        else:
474 1
            self._nodeid_counter[idx] = 1
475 1
        nodeid = ua.NodeId(self._nodeid_counter[idx], idx)
476 1
        with self._lock:  # OK since reentrant lock
477 1
            while True:
478 1
                if nodeid in self._nodes:
479 1
                    nodeid = self.generate_nodeid(idx)
480
                else:
481 1
                    return nodeid
482
483 1
    def keys(self):
484 1
        with self._lock:
485 1
            return self._nodes.keys()
486
487 1
    def empty(self):
488
        """
489
        Delete all nodes in address space
490
        """
491
        with self._lock:
492
            self._nodes = {}
493
494 1
    def dump(self, path):
495
        """
496
        Dump address space as binary to file; note that server must be stopped for this method to work
497
        DO NOT DUMP AN ADDRESS SPACE WHICH IS USING A SHELF (load_aspace_shelf), ONLY CACHED NODES WILL GET DUMPED!
498
        """
499
        # prepare nodes in address space for being serialized
500
        for nodeid, ndata in self._nodes.items():
501
            # if the node has a reference to a method call, remove it so the object can be serialized
502
            if ndata.call is not None:
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 1
    def load(self, path):
509
        """
510
        Load address space from a binary file, overwriting everything in the current address space
511
        """
512
        with open(path, 'rb') as f:
513
            self._nodes = pickle.load(f)
514
515 1
    def make_aspace_shelf(self, path):
516
        """
517
        Make a shelf for containing the nodes from the standard address space; this is typically only done on first
518
        start of the server. Subsequent server starts will load the shelf, nodes are then moved to a cache
519
        by the LazyLoadingDict class when they are accessed. Saving data back to the shelf
520
        is currently NOT supported, it is only used for the default OPC UA standard address space
521
522
        Note: Intended for slow devices, such as Raspberry Pi, to greatly improve start up time
523
        """
524
        s = shelve.open(path, "n", protocol=pickle.HIGHEST_PROTOCOL)
525
        for nodeid, ndata in self._nodes.items():
526
            s[nodeid.to_string()] = ndata
527
        s.close()
528
529 1
    def load_aspace_shelf(self, path):
530
        """
531
        Load the standard address space nodes from a python shelve via LazyLoadingDict as needed.
532
        The dump() method can no longer be used if the address space is being loaded from a shelf
533
534
        Note: Intended for slow devices, such as Raspberry Pi, to greatly improve start up time
535
        """
536
        class LazyLoadingDict(collections.MutableMapping):
537
            """
538
            Special dict that only loads nodes as they are accessed. If a node is accessed it gets copied from the
539
            shelve to the cache dict. All user nodes are saved in the cache ONLY. Saving data back to the shelf
540
            is currently NOT supported
541
            """
542
            def __init__(self, source):
543
                self.source = source  # python shelf
544
                self.cache = {}  # internal dict
545
546
            def __getitem__(self, key):
547
                # try to get the item (node) from the cache, if it isn't there get it from the shelf
548
                try:
549
                    return self.cache[key]
550
                except KeyError:
551
                    node = self.cache[key] = self.source[key.to_string()]
552
                    return node
553
554
            def __setitem__(self, key, value):
555
                # add a new item to the cache; if this item is in the shelf it is not updated
556
                self.cache[key] = value
557
558
            def __contains__(self, key):
559
                return key in self.cache or key.to_string() in self.source
560
561
            def __delitem__(self, key):
562
                # only deleting items from the cache is allowed
563
                del self.cache[key]
564
565
            def __iter__(self):
566
                # only the cache can be iterated over
567
                return iter(self.cache.keys())
568
569
            def __len__(self):
570
                # only returns the length of items in the cache, not unaccessed items in the shelf
571
                return len(self.cache)
572
573
        self._nodes = LazyLoadingDict(shelve.open(path, "r"))
574
575 1
    def get_attribute_value(self, nodeid, attr):
576 1
        with self._lock:
577 1
            self.logger.debug("get attr val: %s %s", nodeid, attr)
578 1
            if nodeid not in self._nodes:
579 1
                dv = ua.DataValue()
580 1
                dv.StatusCode = ua.StatusCode(ua.StatusCodes.BadNodeIdUnknown)
581 1
                return dv
582 1
            node = self._nodes[nodeid]
583 1
            if attr not in node.attributes:
584 1
                dv = ua.DataValue()
585 1
                dv.StatusCode = ua.StatusCode(ua.StatusCodes.BadAttributeIdInvalid)
586 1
                return dv
587 1
            attval = node.attributes[attr]
588 1
            if attval.value_callback:
589
                return attval.value_callback()
590 1
            return attval.value
591
592 1
    def set_attribute_value(self, nodeid, attr, value):
593 1
        with self._lock:
594 1
            self.logger.debug("set attr val: %s %s %s", nodeid, attr, value)
595 1
            if nodeid not in self._nodes:
596 1
                return ua.StatusCode(ua.StatusCodes.BadNodeIdUnknown)
597 1
            node = self._nodes[nodeid]
598 1
            if attr not in node.attributes:
599 1
                return ua.StatusCode(ua.StatusCodes.BadAttributeIdInvalid)
600 1
            if not value.SourceTimestamp:
601 1
                value.SourceTimestamp = datetime.utcnow()
602 1
            if not value.ServerTimestamp:
603 1
                value.ServerTimestamp = datetime.utcnow()
604
605 1
            attval = node.attributes[attr]
606 1
            old = attval.value
607 1
            attval.value = value
608 1
            cbs = []
609 1
            if old.Value != value.Value:  # only send call callback when a value change has happend
610 1
                cbs = list(attval.datachange_callbacks.items())
611
612 1
        for k, v in cbs:
613 1
            try:
614 1
                v(k, value)
615
            except Exception as ex:
616
                self.logger.exception("Error calling datachange callback %s, %s, %s", k, v, ex)
617
618 1
        return ua.StatusCode()
619
620 1
    def add_datachange_callback(self, nodeid, attr, callback):
621 1
        with self._lock:
622 1
            self.logger.debug("set attr callback: %s %s %s", nodeid, attr, callback)
623 1
            if nodeid not in self._nodes:
624
                return ua.StatusCode(ua.StatusCodes.BadNodeIdUnknown), 0
625 1
            node = self._nodes[nodeid]
626 1
            if attr not in node.attributes:
627 1
                return ua.StatusCode(ua.StatusCodes.BadAttributeIdInvalid), 0
628 1
            attval = node.attributes[attr]
629 1
            self._datachange_callback_counter += 1
630 1
            handle = self._datachange_callback_counter
631 1
            attval.datachange_callbacks[handle] = callback
632 1
            self._handle_to_attribute_map[handle] = (nodeid, attr)
633 1
            return ua.StatusCode(), handle
634
635 1
    def delete_datachange_callback(self, handle):
636 1
        with self._lock:
637 1
            if handle in self._handle_to_attribute_map:
638 1
                nodeid, attr = self._handle_to_attribute_map.pop(handle)
639 1
                self._nodes[nodeid].attributes[attr].datachange_callbacks.pop(handle)
640
641 1
    def add_method_callback(self, methodid, callback):
642 1
        with self._lock:
643 1
            node = self._nodes[methodid]
644
            node.call = callback
645