Completed
Pull Request — master (#277)
by Olivier
04:55
created

AddressSpace.empty()   A

Complexity

Conditions 2

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
cc 2
dl 0
loc 6
ccs 0
cts 3
cp 0
crap 6
rs 9.4285
c 0
b 0
f 0
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({})".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:{}, attrs:{}, refs:{})".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.test_bit(al.Value.Value, ua.AccessLevel.CurrentWrite) or not ua.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 not subtypes and ref2.Identifier == ua.ObjectIds.HasSubtype:
112 1
            return False
113
        if ref1.Identifier == ref2.Identifier:
114 1
            return True
115 1
        oktypes = self._get_sub_ref(ref1)
116 1
        if not subtypes and ua.NodeId(ua.ObjectIds.HasSubtype) in oktypes:
117 1
            oktypes.remove(ua.NodeId(ua.ObjectIds.HasSubtype))
118
        return ref2 in oktypes
119 1
120 1
    def _get_sub_ref(self, ref):
121 1
        res = []
122 1
        nodedata = self._aspace[ref]
123 1
        for ref in nodedata.references:
124 1
            if ref.ReferenceTypeId.Identifier == ua.ObjectIds.HasSubtype and ref.IsForward:
125 1
                res.append(ref.NodeId)
126 1
                res += self._get_sub_ref(ref.NodeId)
127
        return res
128 1
129 1
    def _suitable_direction(self, desc, isforward):
130 1
        if desc == ua.BrowseDirection.Both:
131 1
            return True
132 1
        if desc == ua.BrowseDirection.Forward and isforward:
133 1
            return True
134 1
        if desc == ua.BrowseDirection.Inverse and not isforward:
135 1
            return True
136
        return False
137 1
138 1
    def translate_browsepaths_to_nodeids(self, browsepaths):
139 1
        self.logger.debug("translate browsepath: %s", browsepaths)
140 1
        results = []
141 1
        for path in browsepaths:
142 1
            results.append(self._translate_browsepath_to_nodeid(path))
143
        return results
144 1
145 1
    def _translate_browsepath_to_nodeid(self, path):
146 1
        self.logger.debug("looking at path: %s", path)
147 1
        res = ua.BrowsePathResult()
148 1
        if path.StartingNode not in self._aspace:
149 1
            res.StatusCode = ua.StatusCode(ua.StatusCodes.BadNodeIdInvalid)
150 1
            return res
151 1
        current = path.StartingNode
152 1
        for el in path.RelativePath.Elements:
153 1
            nodeid = self._find_element_in_node(el, current)
154 1
            if not nodeid:
155 1
                res.StatusCode = ua.StatusCode(ua.StatusCodes.BadNoMatch)
156 1
                return res
157 1
            current = nodeid
158 1
        target = ua.BrowsePathTarget()
159 1
        target.TargetId = current
160 1
        target.RemainingPathIndex = 4294967295
161 1
        res.Targets = [target]
162
        return res
163 1
164 1
    def _find_element_in_node(self, el, nodeid):
165 1
        nodedata = self._aspace[nodeid]
166
        for ref in nodedata.references:
167 1
            # FIXME: here we should check other arguments!!
168 1
            if ref.BrowseName == el.TargetName:
169 1
                return ref.NodeId
170 1
        self.logger.info("element %s was not found in node %s", el, nodeid)
171
        return None
172
173 1
174
class NodeManagementService(object):
175 1
176 1
    def __init__(self, aspace):
177 1
        self.logger = logging.getLogger(__name__)
178
        self._aspace = aspace
179 1
180 1
    def add_nodes(self, addnodeitems, user=User.Admin):
181 1
        results = []
182 1
        for item in addnodeitems:
183 1
            results.append(self._add_node(item, user))
184
        return results
185 1
186 1
    def _add_node(self, item, user):
187
        result = ua.AddNodesResult()
188
189
        # If Identifier of requested NodeId is null we generate a new NodeId using
190
        # the namespace of the nodeid, this is an extention of the spec to allow
191 1
        # to requests the server to generate a new nodeid in a specified namespace
192 1
        if item.RequestedNewNodeId.has_null_identifier():
193 1
            self.logger.debug("RequestedNewNodeId has null identifier, generating Identifier")
194
            nodedata = NodeData(self._aspace.generate_nodeid(item.RequestedNewNodeId.NamespaceIndex))
195 1
        else:
196
            nodedata = NodeData(item.RequestedNewNodeId)
197 1
198 1
        if nodedata.nodeid in self._aspace:
199 1
            self.logger.warning("AddNodesItem: node already exists")
200 1
            result.StatusCode = ua.StatusCode(ua.StatusCodes.BadNodeIdExists)
201
            return result
202 1
203
        if item.ParentNodeId.is_null():
204
            # self.logger.warning("add_node: creating node %s without parent", nodedata.nodeid)
205 1
            # should return Error here, but the standard namespace define many nodes without parents...
206 1
            pass
207 1
        elif item.ParentNodeId not in self._aspace:
208 1
            self.logger.warning("add_node: while adding node %s, requested parent node %s does not exists", nodedata.nodeid, item.ParentNodeId)
209 1
            result.StatusCode = ua.StatusCode(ua.StatusCodes.BadParentNodeIdInvalid)
210
            return result
211 1
212 1
        if not user == User.Admin:
213 1
            result.StatusCode = ua.StatusCode(ua.StatusCodes.BadUserAccessDenied)
214
            return result
215 1
216
        self._add_node_attributes(nodedata, item)
217
218 1
        # now add our node to db
219
        self._aspace[nodedata.nodeid] = nodedata
220 1
221 1
        if not item.ParentNodeId.is_null():
222 1
            self._add_ref_from_parent(nodedata, item)
223
            self._add_ref_to_parent(nodedata, item, user)
224
225 1
        # add type definition
226 1
        if item.TypeDefinition != ua.NodeId():
227
            self._add_type_definition(nodedata, item, user)
228 1
229 1
        result.StatusCode = ua.StatusCode()
230
        result.AddedNodeId = nodedata.nodeid
231 1
232
        return result
233 1
234
    def _add_node_attributes(self, nodedata, item):
235 1
        # add common attrs
236
        nodedata.attributes[ua.AttributeIds.NodeId] = AttributeValue(
237
            ua.DataValue(ua.Variant(nodedata.nodeid, ua.VariantType.NodeId))
238 1
        )
239
        nodedata.attributes[ua.AttributeIds.BrowseName] = AttributeValue(
240
            ua.DataValue(ua.Variant(item.BrowseName, ua.VariantType.QualifiedName))
241 1
        )
242
        nodedata.attributes[ua.AttributeIds.NodeClass] = AttributeValue(
243
            ua.DataValue(ua.Variant(item.NodeClass, ua.VariantType.Int32))
244
        )
245 1
        # add requested attrs
246
        self._add_nodeattributes(item.NodeAttributes, nodedata)
247 1
248 1
    def _add_ref_from_parent(self, nodedata, item):
249 1
        desc = ua.ReferenceDescription()
250 1
        desc.ReferenceTypeId = item.ReferenceTypeId
251 1
        desc.NodeId = nodedata.nodeid
252 1
        desc.NodeClass = item.NodeClass
253 1
        desc.BrowseName = item.BrowseName
254 1
        desc.DisplayName = ua.LocalizedText(item.BrowseName.Name)
255 1
        desc.TypeDefinition = item.TypeDefinition
256 1
        desc.IsForward = True
257
        self._aspace[item.ParentNodeId].references.append(desc)
258 1
259 1
    def _add_ref_to_parent(self, nodedata, item, user):
260 1
        addref = ua.AddReferencesItem()
261 1
        addref.ReferenceTypeId = item.ReferenceTypeId
262 1
        addref.SourceNodeId = nodedata.nodeid
263 1
        addref.TargetNodeId = item.ParentNodeId
264 1
        addref.TargetNodeClass = self._aspace[item.ParentNodeId].attributes[ua.AttributeIds.NodeClass].value.Value.Value
265 1
        addref.IsForward = False
266
        self._add_reference(addref, user)
267 1
268 1
    def _add_type_definition(self, nodedata, item, user):
269 1
        addref = ua.AddReferencesItem()
270 1
        addref.SourceNodeId = nodedata.nodeid
271 1
        addref.IsForward = True
272 1
        addref.ReferenceTypeId = ua.NodeId(ua.ObjectIds.HasTypeDefinition)
273 1
        addref.TargetNodeId = item.TypeDefinition
274 1
        addref.TargetNodeClass = ua.NodeClass.DataType
275
        self._add_reference(addref, user)
276 1
277 1
    def delete_nodes(self, deletenodeitems, user=User.Admin):
278 1
        results = []
279 1
        for item in deletenodeitems:
280 1
            results.append(self._delete_node(item, user))
281
        return results
282 1
283 1
    def _delete_node(self, item, user):
284
        if user != User.Admin:
285
            return ua.StatusCode(ua.StatusCodes.BadUserAccessDenied)
286 1
287
        if item.NodeId not in self._aspace:
288
            self.logger.warning("DeleteNodesItem: node does not exists")
289
            return ua.StatusCode(ua.StatusCodes.BadNodeIdUnknown)
290 1
291 1
        if item.DeleteTargetReferences:
292 1
            for elem in self._aspace.keys():
293 1
                for rdesc in self._aspace[elem].references:
294 1
                    if rdesc.NodeId == item.NodeId:
295
                        self._aspace[elem].references.remove(rdesc)
296 1
297
        self._delete_node_callbacks(self._aspace[item.NodeId])
298 1
299
        del(self._aspace[item.NodeId])
300 1
301
        return ua.StatusCode()
302 1
303 1
    def _delete_node_callbacks(self, nodedata):
304 1
        if ua.AttributeIds.Value in nodedata.attributes:
305
            for handle, callback in nodedata.attributes[ua.AttributeIds.Value].datachange_callbacks.items():
306
                try:
307
                    callback(handle, None, ua.StatusCode(ua.StatusCodes.BadNodeIdUnknown))
308
                    self._aspace.delete_datachange_callback(handle)
309
                except Exception as ex:
310
                    self.logger.exception("Error calling delete node callback callback %s, %s, %s", nodedata, ua.AttributeIds.Value, ex)
311 1
312 1
    def add_references(self, refs, user=User.Admin):
313 1
        result = []
314 1
        for ref in refs:
315 1
            result.append(self._add_reference(ref, user))
316
        return result
317 1
318 1
    def _add_reference(self, addref, user):
319
        if addref.SourceNodeId not in self._aspace:
320 1
            return ua.StatusCode(ua.StatusCodes.BadSourceNodeIdInvalid)
321 1
        if addref.TargetNodeId not in self._aspace:
322 1
            return ua.StatusCode(ua.StatusCodes.BadTargetNodeIdInvalid)
323
        if user != User.Admin:
324 1
            return ua.StatusCode(ua.StatusCodes.BadUserAccessDenied)
325 1
        rdesc = ua.ReferenceDescription()
326 1
        rdesc.ReferenceTypeId = addref.ReferenceTypeId
327 1
        rdesc.IsForward = addref.IsForward
328 1
        rdesc.NodeId = addref.TargetNodeId
329 1
        rdesc.NodeClass = addref.TargetNodeClass
330 1
        bname = self._aspace.get_attribute_value(addref.TargetNodeId, ua.AttributeIds.BrowseName).Value.Value
331 1
        if bname:
332 1
            rdesc.BrowseName = bname
333 1
        dname = self._aspace.get_attribute_value(addref.TargetNodeId, ua.AttributeIds.DisplayName).Value.Value
334 1
        if dname:
335 1
            rdesc.DisplayName = dname
336 1
        self._aspace[addref.SourceNodeId].references.append(rdesc)
337
        return ua.StatusCode()
338 1
339
    def delete_references(self, refs, user=User.Admin):
340
        result = []
341
        for ref in refs:
342
            result.append(self._delete_reference(ref, user))
343
        return result
344 1
345
    def _delete_reference(self, item, user):
346
        if item.SourceNodeId not in self._aspace:
347
            return ua.StatusCode(ua.StatusCodes.BadSourceNodeIdInvalid)
348
        if item.TargetNodeId not in self._aspace:
349
            return ua.StatusCode(ua.StatusCodes.BadTargetNodeIdInvalid)
350
        if user != User.Admin:
351
            return ua.StatusCode(ua.StatusCodes.BadUserAccessDenied)
352
353
        for rdesc in self._aspace[item.SourceNodeId].references:
354
            if rdesc.NodeId is item.TargetNodeId:
355
                if rdesc.RefrenceTypeId != item.RefrenceTypeId:
356
                    return ua.StatusCode(ua.StatusCodes.BadReferenceTypeIdInvalid)
357
                if rdesc.IsForward == item.IsForward or item.DeleteBidirectional:
358
                    self._aspace[item.SourceNodeId].references.remove(rdesc)
359
360
        for rdesc in self._aspace[item.TargetNodeId].references:
361
            if rdesc.NodeId is item.SourceNodeId:
362
                if rdesc.RefrenceTypeId != item.RefrenceTypeId:
363
                    return ua.StatusCode(ua.StatusCodes.BadReferenceTypeIdInvalid)
364
                if rdesc.IsForward == item.IsForward or item.DeleteBidirectional:
365
                    self._aspace[item.SourceNodeId].references.remove(rdesc)
366
367
        return ua.StatusCode()
368 1
369 1
    def _add_node_attr(self, item, nodedata, name, vtype=None):
370 1
        if item.SpecifiedAttributes & getattr(ua.NodeAttributesMask, name):
371 1
            dv = ua.DataValue(ua.Variant(getattr(item, name), vtype))
372 1
            dv.ServerTimestamp = datetime.utcnow()
373 1
            dv.SourceTimestamp = datetime.utcnow()
374
            nodedata.attributes[getattr(ua.AttributeIds, name)] = AttributeValue(dv)
375 1
376 1
    def _add_nodeattributes(self, item, nodedata):
377 1
        self._add_node_attr(item, nodedata, "AccessLevel", ua.VariantType.Byte)
378 1
        self._add_node_attr(item, nodedata, "ArrayDimensions", ua.VariantType.UInt32)
379 1
        self._add_node_attr(item, nodedata, "BrowseName", ua.VariantType.QualifiedName)
380 1
        self._add_node_attr(item, nodedata, "ContainsNoLoops", ua.VariantType.Boolean)
381 1
        self._add_node_attr(item, nodedata, "DataType", ua.VariantType.NodeId)
382 1
        self._add_node_attr(item, nodedata, "Description", ua.VariantType.LocalizedText)
383 1
        self._add_node_attr(item, nodedata, "DisplayName", ua.VariantType.LocalizedText)
384 1
        self._add_node_attr(item, nodedata, "EventNotifier", ua.VariantType.Byte)
385 1
        self._add_node_attr(item, nodedata, "Executable", ua.VariantType.Boolean)
386 1
        self._add_node_attr(item, nodedata, "Historizing", ua.VariantType.Boolean)
387 1
        self._add_node_attr(item, nodedata, "InverseName", ua.VariantType.LocalizedText)
388 1
        self._add_node_attr(item, nodedata, "IsAbstract", ua.VariantType.Boolean)
389 1
        self._add_node_attr(item, nodedata, "MinimumSamplingInterval", ua.VariantType.Double)
390 1
        self._add_node_attr(item, nodedata, "NodeClass", ua.VariantType.UInt32)
391 1
        self._add_node_attr(item, nodedata, "NodeId", ua.VariantType.NodeId)
392 1
        self._add_node_attr(item, nodedata, "Symmetric", ua.VariantType.Boolean)
393 1
        self._add_node_attr(item, nodedata, "UserAccessLevel", ua.VariantType.Byte)
394 1
        self._add_node_attr(item, nodedata, "UserExecutable", ua.VariantType.Boolean)
395 1
        self._add_node_attr(item, nodedata, "UserWriteMask", ua.VariantType.Byte)
396 1
        self._add_node_attr(item, nodedata, "ValueRank", ua.VariantType.Int32)
397 1
        self._add_node_attr(item, nodedata, "WriteMask", ua.VariantType.UInt32)
398 1
        self._add_node_attr(item, nodedata, "UserWriteMask", ua.VariantType.UInt32)
399
        self._add_node_attr(item, nodedata, "Value")
400
401 1
402
class MethodService(object):
403 1
404 1
    def __init__(self, aspace):
405 1
        self.logger = logging.getLogger(__name__)
406
        self._aspace = aspace
407 1
408 1
    def call(self, methods):
409 1
        results = []
410 1
        for method in methods:
411 1
            results.append(self._call(method))
412
        return results
413 1
414 1
    def _call(self, method):
415 1
        res = ua.CallMethodResult()
416 1
        if method.ObjectId not in self._aspace or method.MethodId not in self._aspace:
417
            res.StatusCode = ua.StatusCode(ua.StatusCodes.BadNodeIdInvalid)
418 1
        else:
419 1
            node = self._aspace[method.MethodId]
420
            if node.call is None:
421
                res.StatusCode = ua.StatusCode(ua.StatusCodes.BadNothingToDo)
422 1
            else:
423 1
                try:
424 1
                    res.OutputArguments = node.call(method.ObjectId, *method.InputArguments)
425 1
                    for _ in method.InputArguments:
426 1
                        res.InputArgumentResults.append(ua.StatusCode())
427 1
                except Exception:
428 1
                    self.logger.exception("Error executing method call %s, an exception was raised: ", method)
429 1
                    res.StatusCode = ua.StatusCode(ua.StatusCodes.BadUnexpectedError)
430
        return res
431
432 1
433
class AddressSpace(object):
434
435
    """
436
    The address space object stores all the nodes og the OPC-UA server
437
    and helper methods.
438
    The methods are threadsafe
439
    """
440 1
441 1
    def __init__(self):
442 1
        self.logger = logging.getLogger(__name__)
443 1
        self._nodes = {}
444 1
        self._lock = RLock()  # FIXME: should use multiple reader, one writter pattern
445 1
        self._datachange_callback_counter = 200
446 1
        self._handle_to_attribute_map = {}
447 1
        self._default_idx = 2
448
        self._nodeid_counter = 2000
449 1
450 1
    def __getitem__(self, nodeid):
451 1
        with self._lock:
452
            return self._nodes.__getitem__(nodeid)
453 1
454 1
    def __setitem__(self, nodeid, value):
455 1
        with self._lock:
456
            return self._nodes.__setitem__(nodeid, value)
457 1
458 1
    def __contains__(self, nodeid):
459 1
        with self._lock:
460
            return self._nodes.__contains__(nodeid)
461 1
462 1
    def __delitem__(self, nodeid):
463 1
        with self._lock:
464
            self._nodes.__delitem__(nodeid)
465 1
466 1
    def generate_nodeid(self, idx=0):
467 1
        if not idx:
468 1
            idx = self._default_idx
469 1
        self._nodeid_counter += 1
470
        return ua.NodeId(self._nodeid_counter, idx)
471 1
472 1
    def keys(self):
473 1
        with self._lock:
474
            return self._nodes.keys()
475 1
476
    def empty(self):
477
        """
478
        Delete all nodes in address space
479
        """
480
        with self._lock:
481
            self._nodes = {}
482
483
    def dump(self, path):
484 1
        """
485
        dump address space as binary to file
486
        """
487
        s = shelve.open(path, "n", protocol = pickle.HIGHEST_PROTOCOL)
488
        for nodeid in self._nodes.keys():
489
            s[nodeid.to_string()] = self._nodes[nodeid]
490
        s.close()
491
492
    def load(self, path):
493
        """
494
        load address space from file, overwritting everything current address space
495
        """
496
        class LazyLoadingDict(collections.MutableMapping):
497
            def __init__(self, source):
498
                self.source = source
499
                self.cache = {}
500
501
            def __getitem__(self, key):
502
                try:
503
                    return self.cache[key]
504
                except KeyError:
505
                    node = self.cache[key] = self.source[key.to_string()]
506
                    return node
507
508
            def __setitem__(self, key, value):
509
                self.cache[key] = value
510
511
            def __contains__(self, key):
512
                return key in self.cache or key.to_string() in self.source
513
514
            def __delitem__(self, key):
515
                raise NotImplementedError
516
517 1
            def __iter__(self):
518 1
                raise NotImplementedError
519 1
520 1
            def __len__(self):
521 1
                raise NotImplementedError
522 1
523 1
        self._nodes = LazyLoadingDict(shelve.open(path, "r"))
524 1
525 1
    def get_attribute_value(self, nodeid, attr):
526 1
        with self._lock:
527 1
            self.logger.debug("get attr val: %s %s", nodeid, attr)
528 1
            if nodeid not in self._nodes:
529 1
                dv = ua.DataValue()
530 1
                dv.StatusCode = ua.StatusCode(ua.StatusCodes.BadNodeIdUnknown)
531
                return dv
532 1
            node = self._nodes[nodeid]
533
            if attr not in node.attributes:
534 1
                dv = ua.DataValue()
535 1
                dv.StatusCode = ua.StatusCode(ua.StatusCodes.BadAttributeIdInvalid)
536 1
                return dv
537 1
            attval = node.attributes[attr]
538 1
            if attval.value_callback:
539 1
                return attval.value_callback()
540 1
            return attval.value
541 1
542 1
    def set_attribute_value(self, nodeid, attr, value):
543 1
        with self._lock:
544 1
            self.logger.debug("set attr val: %s %s %s", nodeid, attr, value)
545 1
            if nodeid not in self._nodes:
546
                return ua.StatusCode(ua.StatusCodes.BadNodeIdUnknown)
547 1
            node = self._nodes[nodeid]
548 1
            if attr not in node.attributes:
549 1
                return ua.StatusCode(ua.StatusCodes.BadAttributeIdInvalid)
550 1
            if not value.SourceTimestamp:
551 1
                value.SourceTimestamp = datetime.utcnow()
552 1
            if not value.ServerTimestamp:
553
                value.ServerTimestamp = datetime.utcnow()
554 1
555 1
            attval = node.attributes[attr]
556 1
            old = attval.value
557
            attval.value = value
558
            cbs = []
559
            if old.Value != value.Value:  # only send call callback when a value change has happend
560 1
                cbs = list(attval.datachange_callbacks.items())
561
562 1
        for k, v in cbs:
563 1
            try:
564 1
                v(k, value)
565 1
            except Exception as ex:
566
                self.logger.exception("Error calling datachange callback %s, %s, %s", k, v, ex)
567 1
568 1
        return ua.StatusCode()
569 1
570 1
    def add_datachange_callback(self, nodeid, attr, callback):
571 1
        with self._lock:
572 1
            self.logger.debug("set attr callback: %s %s %s", nodeid, attr, callback)
573 1
            if nodeid not in self._nodes:
574 1
                return ua.StatusCode(ua.StatusCodes.BadNodeIdUnknown), 0
575 1
            node = self._nodes[nodeid]
576
            if attr not in node.attributes:
577 1
                return ua.StatusCode(ua.StatusCodes.BadAttributeIdInvalid), 0
578 1
            attval = node.attributes[attr]
579 1
            self._datachange_callback_counter += 1
580 1
            handle = self._datachange_callback_counter
581
            attval.datachange_callbacks[handle] = callback
582 1
            self._handle_to_attribute_map[handle] = (nodeid, attr)
583 1
            return ua.StatusCode(), handle
584 1
585 1
    def delete_datachange_callback(self, handle):
586
        with self._lock:
587
            nodeid, attr = self._handle_to_attribute_map.pop(handle)
588
            self._nodes[nodeid].attributes[attr].datachange_callbacks.pop(handle)
589
590
    def add_method_callback(self, methodid, callback):
591
        with self._lock:
592
            node = self._nodes[methodid]
593
            node.call = callback
594