Completed
Push — master ( cc12f5...212d98 )
by Olivier
05:14
created

NodeManagementService._add_reference()   A

Complexity

Conditions 4

Size

Total Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 4.0218

Importance

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