Completed
Pull Request — master (#509)
by
unknown
03:51
created

NodeManagementService._add_ref_from_parent()   A

Complexity

Conditions 2

Size

Total Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 2

Importance

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