Passed
Push — master ( 72629f...de3f23 )
by Olivier
03:10
created

NodeManagementService._add_reference()   B

Complexity

Conditions 6

Size

Total Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 6.0052

Importance

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