Completed
Push — master ( 60866b...5efd50 )
by Olivier
02:47
created

ViewService._suitable_direction()   B

Complexity

Conditions 6

Size

Total Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 6
Metric Value
cc 6
dl 0
loc 8
ccs 8
cts 8
cp 1
crap 6
rs 8
1 1
from threading import RLock
2 1
import logging
3 1
from datetime import datetime
4 1
try:
5 1
    import cPickle as pickle
6 1
except:
7 1
    import pickle
8
9 1
from opcua import ua
10 1
from opcua.server.users import User
11
12
13 1
class AttributeValue(object):
14
15 1
    def __init__(self, value):
16 1
        self.value = value
17 1
        self.value_callback = None
18 1
        self.datachange_callbacks = {}
19
20 1
    def __str__(self):
21
        return "AttributeValue({})".format(self.value)
22 1
    __repr__ = __str__
23
24
25 1
class NodeData(object):
26
27 1
    def __init__(self, nodeid):
28 1
        self.nodeid = nodeid
29 1
        self.attributes = {}
30 1
        self.references = []
31 1
        self.call = None
32
33 1
    def __str__(self):
34
        return "NodeData(id:{}, attrs:{}, refs:{})".format(self.nodeid, self.attributes, self.references)
35 1
    __repr__ = __str__
36
37
38 1
class AttributeService(object):
39
40 1
    def __init__(self, aspace):
41 1
        self.logger = logging.getLogger(__name__)
42 1
        self._aspace = aspace
43
44 1
    def read(self, params):
45 1
        self.logger.debug("read %s", params)
46 1
        res = []
47 1
        for readvalue in params.NodesToRead:
48 1
            res.append(self._aspace.get_attribute_value(readvalue.NodeId, readvalue.AttributeId))
49 1
        return res
50
51 1
    def write(self, params, user=User.Admin):
52 1
        self.logger.debug("write %s as user %s", params, user)
53 1
        res = []
54 1
        for writevalue in params.NodesToWrite:
55 1
            if user != User.Admin:
56 1
                if writevalue.AttributeId != ua.AttributeIds.Value:
57 1
                    res.append(ua.StatusCode(ua.StatusCodes.BadUserAccessDenied))
58 1
                    continue
59 1
                al = self._aspace.get_attribute_value(writevalue.NodeId, ua.AttributeIds.AccessLevel)
60 1
                ual = self._aspace.get_attribute_value(writevalue.NodeId, ua.AttributeIds.UserAccessLevel)
61 1
                if not ua.test_bit(al.Value.Value, ua.AccessLevel.CurrentWrite) or not ua.test_bit(ual.Value.Value, ua.AccessLevel.CurrentWrite):
62 1
                    res.append(ua.StatusCode(ua.StatusCodes.BadUserAccessDenied))
63 1
                    continue
64 1
            res.append(self._aspace.set_attribute_value(writevalue.NodeId, writevalue.AttributeId, writevalue.Value))
65 1
        return res
66
67
68 1
class ViewService(object):
69
70 1
    def __init__(self, aspace):
71 1
        self.logger = logging.getLogger(__name__)
72 1
        self._aspace = aspace
73
74 1
    def browse(self, params):
75 1
        self.logger.debug("browse %s", params)
76 1
        res = []
77 1
        for desc in params.NodesToBrowse:
78 1
            res.append(self._browse(desc))
79 1
        return res
80
81 1
    def _browse(self, desc):
82 1
        res = ua.BrowseResult()
83 1
        if desc.NodeId not in self._aspace:
84 1
            res.StatusCode = ua.StatusCode(ua.StatusCodes.BadNodeIdInvalid)
85 1
            return res
86 1
        node = self._aspace[desc.NodeId]
87 1
        for ref in node.references:
88 1
            if not self._is_suitable_ref(desc, ref):
89 1
                continue
90 1
            res.References.append(ref)
91 1
        return res
92
93 1
    def _is_suitable_ref(self, desc, ref):
94 1
        if not self._suitable_direction(desc.BrowseDirection, ref.IsForward):
95 1
            self.logger.debug("%s is not suitable due to direction", ref)
96 1
            return False
97 1
        if not self._suitable_reftype(desc.ReferenceTypeId, ref.ReferenceTypeId, desc.IncludeSubtypes):
98 1
            self.logger.debug("%s is not suitable due to type", ref)
99 1
            return False
100 1
        if desc.NodeClassMask and ((desc.NodeClassMask & ref.NodeClass) == 0):
101 1
            self.logger.debug("%s is not suitable due to class", ref)
102 1
            return False
103 1
        self.logger.debug("%s is a suitable ref for desc %s", ref, desc)
104 1
        return True
105
106 1
    def _suitable_reftype(self, ref1, ref2, subtypes):
107
        """
108
        """
109 1
        if ref1.Identifier == ref2.Identifier:
110 1
            return True
111
        #TODO: Please check the changes here
112 1
        elif not subtypes:
113 1
            return False
114 1
        oktypes = self._get_sub_ref(ref1)
115 1
        return ref2 in oktypes
116
117 1
    def _get_sub_ref(self, ref):
118 1
        res = []
119 1
        nodedata = self._aspace[ref]
120 1
        for ref in nodedata.references:
121 1
            if ref.ReferenceTypeId.Identifier == ua.ObjectIds.HasSubtype and ref.IsForward:
122 1
                res.append(ref.NodeId)
123 1
                res += self._get_sub_ref(ref.NodeId)
124 1
        return res
125
126 1
    def _suitable_direction(self, desc, isforward):
127 1
        if desc == ua.BrowseDirection.Both:
128 1
            return True
129 1
        if desc == ua.BrowseDirection.Forward and isforward:
130 1
            return True
131 1
        if desc == ua.BrowseDirection.Inverse and not isforward:
132 1
            return True
133 1
        return False
134
135 1
    def translate_browsepaths_to_nodeids(self, browsepaths):
136 1
        self.logger.debug("translate browsepath: %s", browsepaths)
137 1
        results = []
138 1
        for path in browsepaths:
139 1
            results.append(self._translate_browsepath_to_nodeid(path))
140 1
        return results
141
142 1
    def _translate_browsepath_to_nodeid(self, path):
143 1
        self.logger.debug("looking at path: %s", path)
144 1
        res = ua.BrowsePathResult()
145 1
        if path.StartingNode not in self._aspace:
146 1
            res.StatusCode = ua.StatusCode(ua.StatusCodes.BadNodeIdInvalid)
147 1
            return res
148 1
        current = path.StartingNode
149 1
        for el in path.RelativePath.Elements:
150 1
            nodeid = self._find_element_in_node(el, current)
151 1
            if not nodeid:
152 1
                res.StatusCode = ua.StatusCode(ua.StatusCodes.BadNoMatch)
153 1
                return res
154 1
            current = nodeid
155 1
        target = ua.BrowsePathTarget()
156 1
        target.TargetId = current
157 1
        target.RemainingPathIndex = 4294967295
158 1
        res.Targets = [target]
159 1
        return res
160
161 1
    def _find_element_in_node(self, el, nodeid):
162 1
        nodedata = self._aspace[nodeid]
163 1
        for ref in nodedata.references:
164
            # FIXME: here we should check other arguments!!
165 1
            if ref.BrowseName == el.TargetName:
166 1
                return ref.NodeId
167 1
        self.logger.info("element %s was not found in node %s", el, nodeid)
168 1
        return None
169
170
171 1
class NodeManagementService(object):
172
173 1
    def __init__(self, aspace):
174 1
        self.logger = logging.getLogger(__name__)
175 1
        self._aspace = aspace
176
177 1
    def add_nodes(self, addnodeitems, user=User.Admin):
178 1
        results = []
179 1
        for item in addnodeitems:
180 1
            results.append(self._add_node(item, user))
181 1
        return results
182
183 1
    def _add_node(self, item, user):
184 1
        result = ua.AddNodesResult()
185
186
        # If Identifier of requested NodeId is null we generate a new NodeId using
187
        # the namespace of the nodeid, this is an extention of the spec to allow
188
        # to requests the server to generate a new nodeid in a specified namespace
189 1
        if item.RequestedNewNodeId.has_null_identifier():
190 1
            self.logger.debug("RequestedNewNodeId has null identifier, generating Identifier")
191 1
            nodedata = NodeData(self._aspace.generate_nodeid(item.RequestedNewNodeId.NamespaceIndex))
192
        else:
193 1
            nodedata = NodeData(item.RequestedNewNodeId)
194
195 1
        if nodedata.nodeid in self._aspace:
196 1
            self.logger.warning("AddNodesItem: node already exists")
197 1
            result.StatusCode = ua.StatusCode(ua.StatusCodes.BadNodeIdExists)
198 1
            return result
199
200 1
        if item.ParentNodeId.is_null():
201
            # self.logger.warning("add_node: creating node %s without parent", nodedata.nodeid)
202
            # We should return Error here, but the standard namespace seems to define many nodes
203
            # without parents, so ignore...
204 1
            pass
205 1
        elif item.ParentNodeId not in self._aspace:
206 1
            self.logger.warning("add_node: while adding node %s, requested parent node %s does not exists", nodedata.nodeid, item.ParentNodeId)
207 1
            result.StatusCode = ua.StatusCode(ua.StatusCodes.BadParentNodeIdInvalid)
208 1
            return result
209
210 1
        if not user == User.Admin:
211 1
            result.StatusCode = ua.StatusCode(ua.StatusCodes.BadUserAccessDenied)
212 1
            return result
213
214
        # add common attrs
215 1
        nodedata.attributes[ua.AttributeIds.NodeId] = AttributeValue(ua.DataValue(ua.Variant(nodedata.nodeid, ua.VariantType.NodeId)))
216 1
        nodedata.attributes[ua.AttributeIds.BrowseName] = AttributeValue(ua.DataValue(ua.Variant(item.BrowseName, ua.VariantType.QualifiedName)))
217 1
        nodedata.attributes[ua.AttributeIds.NodeClass] = AttributeValue(ua.DataValue(ua.Variant(item.NodeClass, ua.VariantType.Int32)))
218
        # add requested attrs
219 1
        self._add_nodeattributes(item.NodeAttributes, nodedata)
220
221
        # now add our node to db
222 1
        self._aspace[nodedata.nodeid] = nodedata
223
224 1
        if not item.ParentNodeId.is_null():
225 1
            desc = ua.ReferenceDescription()
226 1
            desc.ReferenceTypeId = item.ReferenceTypeId
227 1
            desc.NodeId = nodedata.nodeid
228 1
            desc.NodeClass = item.NodeClass
229 1
            desc.BrowseName = item.BrowseName
230 1
            desc.DisplayName = ua.LocalizedText(item.BrowseName.Name)
231 1
            desc.TypeDefinition = item.TypeDefinition
232 1
            desc.IsForward = True
233 1
            self._aspace[item.ParentNodeId].references.append(desc)
234
235 1
            addref = ua.AddReferencesItem()
236 1
            addref.ReferenceTypeId = item.ReferenceTypeId
237 1
            addref.SourceNodeId = nodedata.nodeid
238 1
            addref.TargetNodeId = item.ParentNodeId
239 1
            addref.TargetNodeClass = self._aspace[item.ParentNodeId].attributes[ua.AttributeIds.NodeClass].value.Value.Value
240 1
            addref.IsForward = False
241 1
            self._add_reference(addref, user)
242
243
        # add type definition
244 1
        if item.TypeDefinition != ua.NodeId():
245 1
            addref = ua.AddReferencesItem()
246 1
            addref.SourceNodeId = nodedata.nodeid
247 1
            addref.IsForward = True
248 1
            addref.ReferenceTypeId = ua.NodeId(ua.ObjectIds.HasTypeDefinition)
249 1
            addref.TargetNodeId = item.TypeDefinition
250 1
            addref.TargetNodeClass = ua.NodeClass.DataType
251 1
            self._add_reference(addref, user)
252
253 1
        result.StatusCode = ua.StatusCode()
254 1
        result.AddedNodeId = nodedata.nodeid
255
256 1
        return result
257
258 1
    def delete_nodes(self, deletenodeitems, user=User.Admin):
259 1
        results = []
260 1
        for item in deletenodeitems:
261 1
            results.append(self._delete_node(item, user))
262 1
        return results
263
264 1
    def _delete_node(self, item, user):
265 1
        if user != User.Admin:
266
            return ua.StatusCode(ua.StatusCodes.BadUserAccessDenied)
267
268 1
        if item.NodeId not in self._aspace:
269
            self.logger.warning("DeleteNodesItem: node does not exists")
270
            return ua.StatusCode(ua.StatusCodes.BadNodeIdUnknown)
271
272 1
        if item.DeleteTargetReferences:
273 1
            for elem in self._aspace.keys():
274 1
                for rdesc in self._aspace[elem].references:
275 1
                    if rdesc.NodeId == item.NodeId:
276 1
                        self._aspace[elem].references.remove(rdesc)
277
278 1
        self._delete_node_callbacks(self._aspace[item.NodeId])
279
280 1
        del(self._aspace[item.NodeId])
281
282 1
        return ua.StatusCode()
283
284 1
    def _delete_node_callbacks(self, nodedata):
285 1
        if ua.AttributeIds.Value in nodedata.attributes:
286 1
            for handle, callback in nodedata.attributes[ua.AttributeIds.Value].datachange_callbacks.items():
287
                try:
288
                    callback(handle, None, ua.StatusCode(ua.StatusCodes.BadNodeIdUnknown))
289
                    self._aspace.delete_datachange_callback(handle)
290
                except Exception as ex:
291
                    self.logger.exception("Error calling delete node callback callback %s, %s, %s", nodedata, ua.AttributeIds.Value, ex)
292
293 1
    def add_references(self, refs, user=User.Admin):
294 1
        result = []
295 1
        for ref in refs:
296 1
            result.append(self._add_reference(ref, user))
297 1
        return result
298
299 1
    def _add_reference(self, addref, user):
300 1
        if addref.SourceNodeId not in self._aspace:
301
            return ua.StatusCode(ua.StatusCodes.BadSourceNodeIdInvalid)
302 1
        if addref.TargetNodeId not in self._aspace:
303 1
            return ua.StatusCode(ua.StatusCodes.BadTargetNodeIdInvalid)
304 1
        if user != User.Admin:
305
            return ua.StatusCode(ua.StatusCodes.BadUserAccessDenied)
306 1
        rdesc = ua.ReferenceDescription()
307 1
        rdesc.ReferenceTypeId = addref.ReferenceTypeId
308 1
        rdesc.IsForward = addref.IsForward
309 1
        rdesc.NodeId = addref.TargetNodeId
310 1
        rdesc.NodeClass = addref.TargetNodeClass
311 1
        bname = self._aspace.get_attribute_value(addref.TargetNodeId, ua.AttributeIds.BrowseName).Value.Value
312 1
        if bname:
313 1
            rdesc.BrowseName = bname
314 1
        dname = self._aspace.get_attribute_value(addref.TargetNodeId, ua.AttributeIds.DisplayName).Value.Value
315 1
        if dname:
316 1
            rdesc.DisplayName = dname
317 1
        self._aspace[addref.SourceNodeId].references.append(rdesc)
318 1
        return ua.StatusCode()
319
320 1
    def delete_references(self, refs, user=User.Admin):
321
        result = []
322
        for ref in refs:
323
            result.append(self._delete_reference(ref, user))
324
        return result
325
326 1
    def _delete_reference(self, item, user):
327
        if item.SourceNodeId not in self._aspace:
328
            return ua.StatusCode(ua.StatusCodes.BadSourceNodeIdInvalid)
329
        if item.TargetNodeId not in self._aspace:
330
            return ua.StatusCode(ua.StatusCodes.BadTargetNodeIdInvalid)
331
        if user != User.Admin:
332
            return ua.StatusCode(ua.StatusCodes.BadUserAccessDenied)
333
334
        for rdesc in self._aspace[item.SourceNodeId].references:
335
            if rdesc.NodeId is item.TargetNodeId:
336
                if rdesc.RefrenceTypeId != item.RefrenceTypeId:
337
                    return ua.StatusCode(ua.StatusCodes.BadReferenceTypeIdInvalid)
338
                if rdesc.IsForward == item.IsForward or item.DeleteBidirectional:
339
                    self._aspace[item.SourceNodeId].references.remove(rdesc)
340
341
        for rdesc in self._aspace[item.TargetNodeId].references:
342
            if rdesc.NodeId is item.SourceNodeId:
343
                if rdesc.RefrenceTypeId != item.RefrenceTypeId:
344
                    return ua.StatusCode(ua.StatusCodes.BadReferenceTypeIdInvalid)
345
                if rdesc.IsForward == item.IsForward or item.DeleteBidirectional:
346
                    self._aspace[item.SourceNodeId].references.remove(rdesc)
347
348
        return ua.StatusCode()
349
350 1
    def _add_node_attr(self, item, nodedata, name, vtype=None):
351 1
        if item.SpecifiedAttributes & getattr(ua.NodeAttributesMask, name):
352 1
            dv = ua.DataValue(ua.Variant(getattr(item, name), vtype))
353 1
            dv.ServerTimestamp = datetime.utcnow()
354 1
            dv.SourceTimestamp = datetime.utcnow()
355 1
            nodedata.attributes[getattr(ua.AttributeIds, name)] = AttributeValue(dv)
356
357 1
    def _add_nodeattributes(self, item, nodedata):
358 1
        self._add_node_attr(item, nodedata, "AccessLevel", ua.VariantType.Byte)
359 1
        self._add_node_attr(item, nodedata, "ArrayDimensions", ua.VariantType.Int32)
360 1
        self._add_node_attr(item, nodedata, "BrowseName", ua.VariantType.QualifiedName)
361 1
        self._add_node_attr(item, nodedata, "ContainsNoLoops", ua.VariantType.Boolean)
362 1
        self._add_node_attr(item, nodedata, "DataType", ua.VariantType.NodeId)
363 1
        self._add_node_attr(item, nodedata, "Description", ua.VariantType.LocalizedText)
364 1
        self._add_node_attr(item, nodedata, "DisplayName", ua.VariantType.LocalizedText)
365 1
        self._add_node_attr(item, nodedata, "EventNotifier", ua.VariantType.Byte)
366 1
        self._add_node_attr(item, nodedata, "Executable", ua.VariantType.Boolean)
367 1
        self._add_node_attr(item, nodedata, "Historizing", ua.VariantType.Boolean)
368 1
        self._add_node_attr(item, nodedata, "InverseName", ua.VariantType.LocalizedText)
369 1
        self._add_node_attr(item, nodedata, "IsAbstract", ua.VariantType.Boolean)
370 1
        self._add_node_attr(item, nodedata, "MinimumSamplingInterval", ua.VariantType.Double)
371 1
        self._add_node_attr(item, nodedata, "NodeClass", ua.VariantType.UInt32)
372 1
        self._add_node_attr(item, nodedata, "NodeId", ua.VariantType.NodeId)
373 1
        self._add_node_attr(item, nodedata, "Symmetric", ua.VariantType.Boolean)
374 1
        self._add_node_attr(item, nodedata, "UserAccessLevel", ua.VariantType.Byte)
375 1
        self._add_node_attr(item, nodedata, "UserExecutable", ua.VariantType.Byte)
376 1
        self._add_node_attr(item, nodedata, "UserWriteMask", ua.VariantType.Byte)
377 1
        self._add_node_attr(item, nodedata, "ValueRank", ua.VariantType.Int32)
378 1
        self._add_node_attr(item, nodedata, "WriteMask", ua.VariantType.Byte)
379 1
        self._add_node_attr(item, nodedata, "UserWriteMask", ua.VariantType.Byte)
380 1
        self._add_node_attr(item, nodedata, "Value")
381
382
383 1
class MethodService(object):
384
385 1
    def __init__(self, aspace):
386 1
        self.logger = logging.getLogger(__name__)
387 1
        self._aspace = aspace
388
389 1
    def call(self, methods):
390 1
        results = []
391 1
        for method in methods:
392 1
            results.append(self._call(method))
393 1
        return results
394
395 1
    def _call(self, method):
396 1
        res = ua.CallMethodResult()
397 1
        if method.ObjectId not in self._aspace or method.MethodId not in self._aspace:
398 1
            res.StatusCode = ua.StatusCode(ua.StatusCodes.BadNodeIdInvalid)
399
        else:
400 1
            node = self._aspace[method.MethodId]
401 1
            if node.call is None:
402
                res.StatusCode = ua.StatusCode(ua.StatusCodes.BadNothingToDo)
403
            else:
404 1
                try:
405 1
                    res.OutputArguments = node.call(method.ObjectId, *method.InputArguments)
406 1
                    for _ in method.InputArguments:
407 1
                        res.InputArgumentResults.append(ua.StatusCode())
408 1
                except Exception:
409 1
                    self.logger.exception("Error executing method call %s, an exception was raised: ", method)
410 1
                    res.StatusCode = ua.StatusCode(ua.StatusCodes.BadUnexpectedError)
411 1
        return res
412
413
414 1
class AddressSpace(object):
415
416
    """
417
    The address space object stores all the nodes og the OPC-UA server
418
    and helper methods.
419
    The methods are threadsafe
420
    """
421
422 1
    def __init__(self):
423 1
        self.logger = logging.getLogger(__name__)
424 1
        self._nodes = {}
425 1
        self._lock = RLock()  # FIXME: should use multiple reader, one writter pattern
426 1
        self._datachange_callback_counter = 200
427 1
        self._handle_to_attribute_map = {}
428 1
        self._default_idx = 2
429 1
        self._nodeid_counter = 2000
430
431 1
    def __getitem__(self, nodeid):
432 1
        with self._lock:
433 1
            return self._nodes.__getitem__(nodeid)
434
435 1
    def __setitem__(self, nodeid, value):
436 1
        with self._lock:
437 1
            return self._nodes.__setitem__(nodeid, value)
438
439 1
    def __contains__(self, nodeid):
440 1
        with self._lock:
441 1
            return self._nodes.__contains__(nodeid)
442
443 1
    def __delitem__(self, nodeid):
444 1
        with self._lock:
445 1
            self._nodes.__delitem__(nodeid)
446
447 1
    def generate_nodeid(self, idx=0):
448 1
        if not idx:
449 1
            idx = self._default_idx
450 1
        self._nodeid_counter += 1
451 1
        return ua.NodeId(self._nodeid_counter, idx)
452
453 1
    def keys(self):
454 1
        with self._lock:
455 1
            return self._nodes.keys()
456
457 1
    def dump(self, path):
458
        """
459
        dump address space as binary to file
460
        """
461
        with open(path, 'wb') as f:
462
            pickle.dump(self._nodes, f, pickle.HIGHEST_PROTOCOL)
463
464 1
    def load(self, path):
465
        """
466
        load address space from file, overwritting everything current address space
467
        """
468
        with open(path, 'rb') as f:
469
            self._nodes = pickle.load(f)
470
471 1
    def get_attribute_value(self, nodeid, attr):
472 1
        with self._lock:
473
            #self.logger.debug("get attr val: %s %s", nodeid, attr)
474 1
            if nodeid not in self._nodes:
475 1
                dv = ua.DataValue()
476 1
                dv.StatusCode = ua.StatusCode(ua.StatusCodes.BadNodeIdUnknown)
477 1
                return dv
478 1
            node = self._nodes[nodeid]
479 1
            if attr not in node.attributes:
480 1
                dv = ua.DataValue()
481 1
                dv.StatusCode = ua.StatusCode(ua.StatusCodes.BadAttributeIdInvalid)
482 1
                return dv
483 1
            attval = node.attributes[attr]
484 1
            if attval.value_callback:
485
                return attval.value_callback()
486 1
            return attval.value
487
488 1
    def set_attribute_value(self, nodeid, attr, value):
489 1
        with self._lock:
490 1
            self.logger.debug("set attr val: %s %s %s", nodeid, attr, value)
491 1
            if nodeid not in self._nodes:
492 1
                return ua.StatusCode(ua.StatusCodes.BadNodeIdUnknown)
493 1
            node = self._nodes[nodeid]
494 1
            if attr not in node.attributes:
495 1
                return ua.StatusCode(ua.StatusCodes.BadAttributeIdInvalid)
496 1
            if not value.SourceTimestamp:
497 1
                value.SourceTimestamp = datetime.utcnow()
498 1
            if not value.ServerTimestamp:
499 1
                value.ServerTimestamp = datetime.utcnow()
500
501 1
            attval = node.attributes[attr]
502 1
            old = attval.value
503 1
            attval.value = value
504 1
            cbs = []
505 1
            if old.Value != value.Value:  # only send call callback when a value change has happend
506 1
                cbs = list(attval.datachange_callbacks.items())
507
508 1
        for k, v in cbs:
509 1
            try:
510 1
                v(k, value)
511
            except Exception as ex:
512
                self.logger.exception("Error calling datachange callback %s, %s, %s", k, v, ex)
513
514 1
        return ua.StatusCode()
515
516 1
    def add_datachange_callback(self, nodeid, attr, callback):
517 1
        with self._lock:
518 1
            self.logger.debug("set attr callback: %s %s %s", nodeid, attr, callback)
519 1
            if nodeid not in self._nodes:
520
                return ua.StatusCode(ua.StatusCodes.BadNodeIdUnknown), 0
521 1
            node = self._nodes[nodeid]
522 1
            if attr not in node.attributes:
523 1
                return ua.StatusCode(ua.StatusCodes.BadAttributeIdInvalid), 0
524 1
            attval = node.attributes[attr]
525 1
            self._datachange_callback_counter += 1
526 1
            handle = self._datachange_callback_counter
527 1
            attval.datachange_callbacks[handle] = callback
528 1
            self._handle_to_attribute_map[handle] = (nodeid, attr)
529 1
            return ua.StatusCode(), handle
530
531 1
    def delete_datachange_callback(self, handle):
532 1
        with self._lock:
533 1
            nodeid, attr = self._handle_to_attribute_map.pop(handle)
534 1
            self._nodes[nodeid].attributes[attr].datachange_callbacks.pop(handle)
535
536 1
    def add_method_callback(self, methodid, callback):
537 1
        with self._lock:
538 1
            node = self._nodes[methodid]
539
            node.call = callback
540