Completed
Pull Request — master (#509)
by
unknown
04:17
created

NodeManagementService   D

Complexity

Total Complexity 58

Size/Duplication

Total Lines 231
Duplicated Lines 0 %

Test Coverage

Coverage 81.67%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
dl 0
loc 231
ccs 147
cts 180
cp 0.8167
rs 4.8387
c 2
b 0
f 0
wmc 58

17 Methods

Rating   Name   Duplication   Size   Complexity  
A __init__() 0 3 1
A delete_nodes() 0 5 2
A add_references() 0 5 2
A _add_type_definition() 0 8 1
B _delete_node() 0 19 7
A _add_node_attributes() 0 13 1
A add_nodes() 0 5 2
A _add_ref_from_parent() 0 10 1
A _delete_node_callbacks() 0 8 4
D _add_node() 0 47 8
A _add_ref_to_parent() 0 8 1
B _add_nodeattributes() 0 24 1
A _add_node_attr() 0 6 2
A try_add_references() 0 4 3
B _add_reference() 0 20 6
F _delete_reference() 0 23 14
A delete_references() 0 5 2

How to fix   Complexity   

Complex Class

Complex classes like NodeManagementService often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

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