00001 from threading import RLock
00002 import logging
00003 from datetime import datetime
00004 import collections
00005 import shelve
00006 try:
00007 import cPickle as pickle
00008 except:
00009 import pickle
00010
00011 from opcua import ua
00012 from opcua.server.users import User
00013
00014
00015 class AttributeValue(object):
00016
00017 def __init__(self, value):
00018 self.value = value
00019 self.value_callback = None
00020 self.datachange_callbacks = {}
00021
00022 def __str__(self):
00023 return "AttributeValue({0})".format(self.value)
00024 __repr__ = __str__
00025
00026
00027 class NodeData(object):
00028
00029 def __init__(self, nodeid):
00030 self.nodeid = nodeid
00031 self.attributes = {}
00032 self.references = []
00033 self.call = None
00034
00035 def __str__(self):
00036 return "NodeData(id:{0}, attrs:{1}, refs:{2})".format(self.nodeid, self.attributes, self.references)
00037 __repr__ = __str__
00038
00039
00040 class AttributeService(object):
00041
00042 def __init__(self, aspace):
00043 self.logger = logging.getLogger(__name__)
00044 self._aspace = aspace
00045
00046 def read(self, params):
00047 self.logger.debug("read %s", params)
00048 res = []
00049 for readvalue in params.NodesToRead:
00050 res.append(self._aspace.get_attribute_value(readvalue.NodeId, readvalue.AttributeId))
00051 return res
00052
00053 def write(self, params, user=User.Admin):
00054 self.logger.debug("write %s as user %s", params, user)
00055 res = []
00056 for writevalue in params.NodesToWrite:
00057 if user != User.Admin:
00058 if writevalue.AttributeId != ua.AttributeIds.Value:
00059 res.append(ua.StatusCode(ua.StatusCodes.BadUserAccessDenied))
00060 continue
00061 al = self._aspace.get_attribute_value(writevalue.NodeId, ua.AttributeIds.AccessLevel)
00062 ual = self._aspace.get_attribute_value(writevalue.NodeId, ua.AttributeIds.UserAccessLevel)
00063 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):
00064 res.append(ua.StatusCode(ua.StatusCodes.BadUserAccessDenied))
00065 continue
00066 res.append(self._aspace.set_attribute_value(writevalue.NodeId, writevalue.AttributeId, writevalue.Value))
00067 return res
00068
00069
00070 class ViewService(object):
00071
00072 def __init__(self, aspace):
00073 self.logger = logging.getLogger(__name__)
00074 self._aspace = aspace
00075
00076 def browse(self, params):
00077 self.logger.debug("browse %s", params)
00078 res = []
00079 for desc in params.NodesToBrowse:
00080 res.append(self._browse(desc))
00081 return res
00082
00083 def _browse(self, desc):
00084 res = ua.BrowseResult()
00085 if desc.NodeId not in self._aspace:
00086 res.StatusCode = ua.StatusCode(ua.StatusCodes.BadNodeIdInvalid)
00087 return res
00088 node = self._aspace[desc.NodeId]
00089 for ref in node.references:
00090 if not self._is_suitable_ref(desc, ref):
00091 continue
00092 res.References.append(ref)
00093 return res
00094
00095 def _is_suitable_ref(self, desc, ref):
00096 if not self._suitable_direction(desc.BrowseDirection, ref.IsForward):
00097 self.logger.debug("%s is not suitable due to direction", ref)
00098 return False
00099 if not self._suitable_reftype(desc.ReferenceTypeId, ref.ReferenceTypeId, desc.IncludeSubtypes):
00100 self.logger.debug("%s is not suitable due to type", ref)
00101 return False
00102 if desc.NodeClassMask and ((desc.NodeClassMask & ref.NodeClass) == 0):
00103 self.logger.debug("%s is not suitable due to class", ref)
00104 return False
00105 self.logger.debug("%s is a suitable ref for desc %s", ref, desc)
00106 return True
00107
00108 def _suitable_reftype(self, ref1, ref2, subtypes):
00109 """
00110 """
00111 if not subtypes and ref2.Identifier == ua.ObjectIds.HasSubtype:
00112 return False
00113 if ref1.Identifier == ref2.Identifier:
00114 return True
00115 oktypes = self._get_sub_ref(ref1)
00116 if not subtypes and ua.NodeId(ua.ObjectIds.HasSubtype) in oktypes:
00117 oktypes.remove(ua.NodeId(ua.ObjectIds.HasSubtype))
00118 return ref2 in oktypes
00119
00120 def _get_sub_ref(self, ref):
00121 res = []
00122 nodedata = self._aspace[ref]
00123 if nodedata is not None:
00124 for ref in nodedata.references:
00125 if ref.ReferenceTypeId.Identifier == ua.ObjectIds.HasSubtype and ref.IsForward:
00126 res.append(ref.NodeId)
00127 res += self._get_sub_ref(ref.NodeId)
00128 return res
00129
00130 def _suitable_direction(self, desc, isforward):
00131 if desc == ua.BrowseDirection.Both:
00132 return True
00133 if desc == ua.BrowseDirection.Forward and isforward:
00134 return True
00135 if desc == ua.BrowseDirection.Inverse and not isforward:
00136 return True
00137 return False
00138
00139 def translate_browsepaths_to_nodeids(self, browsepaths):
00140 self.logger.debug("translate browsepath: %s", browsepaths)
00141 results = []
00142 for path in browsepaths:
00143 results.append(self._translate_browsepath_to_nodeid(path))
00144 return results
00145
00146 def _translate_browsepath_to_nodeid(self, path):
00147 self.logger.debug("looking at path: %s", path)
00148 res = ua.BrowsePathResult()
00149 if path.StartingNode not in self._aspace:
00150 res.StatusCode = ua.StatusCode(ua.StatusCodes.BadNodeIdInvalid)
00151 return res
00152 current = path.StartingNode
00153 for el in path.RelativePath.Elements:
00154 nodeid = self._find_element_in_node(el, current)
00155 if not nodeid:
00156 res.StatusCode = ua.StatusCode(ua.StatusCodes.BadNoMatch)
00157 return res
00158 current = nodeid
00159 target = ua.BrowsePathTarget()
00160 target.TargetId = current
00161 target.RemainingPathIndex = 4294967295
00162 res.Targets = [target]
00163 return res
00164
00165 def _find_element_in_node(self, el, nodeid):
00166 nodedata = self._aspace[nodeid]
00167 for ref in nodedata.references:
00168
00169 if ref.BrowseName == el.TargetName:
00170 return ref.NodeId
00171 self.logger.info("element %s was not found in node %s", el, nodeid)
00172 return None
00173
00174
00175 class NodeManagementService(object):
00176
00177 def __init__(self, aspace):
00178 self.logger = logging.getLogger(__name__)
00179 self._aspace = aspace
00180
00181 def add_nodes(self, addnodeitems, user=User.Admin):
00182 results = []
00183 for item in addnodeitems:
00184 results.append(self._add_node(item, user))
00185 return results
00186
00187 def _add_node(self, item, user):
00188 result = ua.AddNodesResult()
00189
00190
00191
00192
00193 if item.RequestedNewNodeId.has_null_identifier():
00194 self.logger.debug("RequestedNewNodeId has null identifier, generating Identifier")
00195 nodedata = NodeData(self._aspace.generate_nodeid(item.RequestedNewNodeId.NamespaceIndex))
00196 else:
00197 nodedata = NodeData(item.RequestedNewNodeId)
00198
00199 if nodedata.nodeid in self._aspace:
00200 self.logger.warning("AddNodesItem: node already exists")
00201 result.StatusCode = ua.StatusCode(ua.StatusCodes.BadNodeIdExists)
00202 return result
00203
00204 if item.ParentNodeId.is_null():
00205
00206
00207 pass
00208 elif item.ParentNodeId not in self._aspace:
00209 self.logger.warning("add_node: while adding node %s, requested parent node %s does not exists", nodedata.nodeid, item.ParentNodeId)
00210 result.StatusCode = ua.StatusCode(ua.StatusCodes.BadParentNodeIdInvalid)
00211 return result
00212
00213 if not user == User.Admin:
00214 result.StatusCode = ua.StatusCode(ua.StatusCodes.BadUserAccessDenied)
00215 return result
00216
00217 self._add_node_attributes(nodedata, item)
00218
00219
00220 self._aspace[nodedata.nodeid] = nodedata
00221
00222 if not item.ParentNodeId.is_null():
00223 self._add_ref_from_parent(nodedata, item)
00224 self._add_ref_to_parent(nodedata, item, user)
00225
00226
00227 if item.TypeDefinition != ua.NodeId():
00228 self._add_type_definition(nodedata, item, user)
00229
00230 result.StatusCode = ua.StatusCode()
00231 result.AddedNodeId = nodedata.nodeid
00232
00233 return result
00234
00235 def _add_node_attributes(self, nodedata, item):
00236
00237 nodedata.attributes[ua.AttributeIds.NodeId] = AttributeValue(
00238 ua.DataValue(ua.Variant(nodedata.nodeid, ua.VariantType.NodeId))
00239 )
00240 nodedata.attributes[ua.AttributeIds.BrowseName] = AttributeValue(
00241 ua.DataValue(ua.Variant(item.BrowseName, ua.VariantType.QualifiedName))
00242 )
00243 nodedata.attributes[ua.AttributeIds.NodeClass] = AttributeValue(
00244 ua.DataValue(ua.Variant(item.NodeClass, ua.VariantType.Int32))
00245 )
00246
00247 self._add_nodeattributes(item.NodeAttributes, nodedata)
00248
00249 def _add_ref_from_parent(self, nodedata, item):
00250 desc = ua.ReferenceDescription()
00251 desc.ReferenceTypeId = item.ReferenceTypeId
00252 desc.NodeId = nodedata.nodeid
00253 desc.NodeClass = item.NodeClass
00254 desc.BrowseName = item.BrowseName
00255 desc.DisplayName = ua.LocalizedText(item.BrowseName.Name)
00256 desc.TypeDefinition = item.TypeDefinition
00257 desc.IsForward = True
00258 self._aspace[item.ParentNodeId].references.append(desc)
00259
00260 def _add_ref_to_parent(self, nodedata, item, user):
00261 addref = ua.AddReferencesItem()
00262 addref.ReferenceTypeId = item.ReferenceTypeId
00263 addref.SourceNodeId = nodedata.nodeid
00264 addref.TargetNodeId = item.ParentNodeId
00265 addref.TargetNodeClass = self._aspace[item.ParentNodeId].attributes[ua.AttributeIds.NodeClass].value.Value.Value
00266 addref.IsForward = False
00267 self._add_reference(addref, user)
00268
00269 def _add_type_definition(self, nodedata, item, user):
00270 addref = ua.AddReferencesItem()
00271 addref.SourceNodeId = nodedata.nodeid
00272 addref.IsForward = True
00273 addref.ReferenceTypeId = ua.NodeId(ua.ObjectIds.HasTypeDefinition)
00274 addref.TargetNodeId = item.TypeDefinition
00275 addref.TargetNodeClass = ua.NodeClass.DataType
00276 self._add_reference(addref, user)
00277
00278 def delete_nodes(self, deletenodeitems, user=User.Admin):
00279 results = []
00280 for item in deletenodeitems.NodesToDelete:
00281 results.append(self._delete_node(item, user))
00282 return results
00283
00284 def _delete_node(self, item, user):
00285 if user != User.Admin:
00286 return ua.StatusCode(ua.StatusCodes.BadUserAccessDenied)
00287
00288 if item.NodeId not in self._aspace:
00289 self.logger.warning("DeleteNodesItem: node does not exists")
00290 return ua.StatusCode(ua.StatusCodes.BadNodeIdUnknown)
00291
00292 if item.DeleteTargetReferences:
00293 for elem in self._aspace.keys():
00294 for rdesc in self._aspace[elem].references:
00295 if rdesc.NodeId == item.NodeId:
00296 self._aspace[elem].references.remove(rdesc)
00297
00298 self._delete_node_callbacks(self._aspace[item.NodeId])
00299
00300 del(self._aspace[item.NodeId])
00301
00302 return ua.StatusCode()
00303
00304 def _delete_node_callbacks(self, nodedata):
00305 if ua.AttributeIds.Value in nodedata.attributes:
00306 for handle, callback in nodedata.attributes[ua.AttributeIds.Value].datachange_callbacks.items():
00307 try:
00308 callback(handle, None, ua.StatusCode(ua.StatusCodes.BadNodeIdUnknown))
00309 self._aspace.delete_datachange_callback(handle)
00310 except Exception as ex:
00311 self.logger.exception("Error calling delete node callback callback %s, %s, %s", nodedata, ua.AttributeIds.Value, ex)
00312
00313 def add_references(self, refs, user=User.Admin):
00314 result = []
00315 for ref in refs:
00316 result.append(self._add_reference(ref, user))
00317 return result
00318
00319 def _add_reference(self, addref, user):
00320 if addref.SourceNodeId not in self._aspace:
00321 return ua.StatusCode(ua.StatusCodes.BadSourceNodeIdInvalid)
00322 if addref.TargetNodeId not in self._aspace:
00323 return ua.StatusCode(ua.StatusCodes.BadTargetNodeIdInvalid)
00324 if user != User.Admin:
00325 return ua.StatusCode(ua.StatusCodes.BadUserAccessDenied)
00326 rdesc = ua.ReferenceDescription()
00327 rdesc.ReferenceTypeId = addref.ReferenceTypeId
00328 rdesc.IsForward = addref.IsForward
00329 rdesc.NodeId = addref.TargetNodeId
00330 rdesc.NodeClass = addref.TargetNodeClass
00331 bname = self._aspace.get_attribute_value(addref.TargetNodeId, ua.AttributeIds.BrowseName).Value.Value
00332 if bname:
00333 rdesc.BrowseName = bname
00334 dname = self._aspace.get_attribute_value(addref.TargetNodeId, ua.AttributeIds.DisplayName).Value.Value
00335 if dname:
00336 rdesc.DisplayName = dname
00337 self._aspace[addref.SourceNodeId].references.append(rdesc)
00338 return ua.StatusCode()
00339
00340 def delete_references(self, refs, user=User.Admin):
00341 result = []
00342 for ref in refs:
00343 result.append(self._delete_reference(ref, user))
00344 return result
00345
00346 def _delete_reference(self, item, user):
00347 if item.SourceNodeId not in self._aspace:
00348 return ua.StatusCode(ua.StatusCodes.BadSourceNodeIdInvalid)
00349 if item.TargetNodeId not in self._aspace:
00350 return ua.StatusCode(ua.StatusCodes.BadTargetNodeIdInvalid)
00351 if user != User.Admin:
00352 return ua.StatusCode(ua.StatusCodes.BadUserAccessDenied)
00353
00354 for rdesc in self._aspace[item.SourceNodeId].references:
00355 if rdesc.NodeId is item.TargetNodeId:
00356 if rdesc.RefrenceTypeId != item.RefrenceTypeId:
00357 return ua.StatusCode(ua.StatusCodes.BadReferenceTypeIdInvalid)
00358 if rdesc.IsForward == item.IsForward or item.DeleteBidirectional:
00359 self._aspace[item.SourceNodeId].references.remove(rdesc)
00360
00361 for rdesc in self._aspace[item.TargetNodeId].references:
00362 if rdesc.NodeId is item.SourceNodeId:
00363 if rdesc.RefrenceTypeId != item.RefrenceTypeId:
00364 return ua.StatusCode(ua.StatusCodes.BadReferenceTypeIdInvalid)
00365 if rdesc.IsForward == item.IsForward or item.DeleteBidirectional:
00366 self._aspace[item.SourceNodeId].references.remove(rdesc)
00367
00368 return ua.StatusCode()
00369
00370 def _add_node_attr(self, item, nodedata, name, vtype=None):
00371 if item.SpecifiedAttributes & getattr(ua.NodeAttributesMask, name):
00372 dv = ua.DataValue(ua.Variant(getattr(item, name), vtype))
00373 dv.ServerTimestamp = datetime.utcnow()
00374 dv.SourceTimestamp = datetime.utcnow()
00375 nodedata.attributes[getattr(ua.AttributeIds, name)] = AttributeValue(dv)
00376
00377 def _add_nodeattributes(self, item, nodedata):
00378 self._add_node_attr(item, nodedata, "AccessLevel", ua.VariantType.Byte)
00379 self._add_node_attr(item, nodedata, "ArrayDimensions", ua.VariantType.UInt32)
00380 self._add_node_attr(item, nodedata, "BrowseName", ua.VariantType.QualifiedName)
00381 self._add_node_attr(item, nodedata, "ContainsNoLoops", ua.VariantType.Boolean)
00382 self._add_node_attr(item, nodedata, "DataType", ua.VariantType.NodeId)
00383 self._add_node_attr(item, nodedata, "Description", ua.VariantType.LocalizedText)
00384 self._add_node_attr(item, nodedata, "DisplayName", ua.VariantType.LocalizedText)
00385 self._add_node_attr(item, nodedata, "EventNotifier", ua.VariantType.Byte)
00386 self._add_node_attr(item, nodedata, "Executable", ua.VariantType.Boolean)
00387 self._add_node_attr(item, nodedata, "Historizing", ua.VariantType.Boolean)
00388 self._add_node_attr(item, nodedata, "InverseName", ua.VariantType.LocalizedText)
00389 self._add_node_attr(item, nodedata, "IsAbstract", ua.VariantType.Boolean)
00390 self._add_node_attr(item, nodedata, "MinimumSamplingInterval", ua.VariantType.Double)
00391 self._add_node_attr(item, nodedata, "NodeClass", ua.VariantType.UInt32)
00392 self._add_node_attr(item, nodedata, "NodeId", ua.VariantType.NodeId)
00393 self._add_node_attr(item, nodedata, "Symmetric", ua.VariantType.Boolean)
00394 self._add_node_attr(item, nodedata, "UserAccessLevel", ua.VariantType.Byte)
00395 self._add_node_attr(item, nodedata, "UserExecutable", ua.VariantType.Boolean)
00396 self._add_node_attr(item, nodedata, "UserWriteMask", ua.VariantType.Byte)
00397 self._add_node_attr(item, nodedata, "ValueRank", ua.VariantType.Int32)
00398 self._add_node_attr(item, nodedata, "WriteMask", ua.VariantType.UInt32)
00399 self._add_node_attr(item, nodedata, "UserWriteMask", ua.VariantType.UInt32)
00400 self._add_node_attr(item, nodedata, "Value")
00401
00402
00403 class MethodService(object):
00404
00405 def __init__(self, aspace):
00406 self.logger = logging.getLogger(__name__)
00407 self._aspace = aspace
00408
00409 def call(self, methods):
00410 results = []
00411 for method in methods:
00412 results.append(self._call(method))
00413 return results
00414
00415 def _call(self, method):
00416 res = ua.CallMethodResult()
00417 if method.ObjectId not in self._aspace or method.MethodId not in self._aspace:
00418 res.StatusCode = ua.StatusCode(ua.StatusCodes.BadNodeIdInvalid)
00419 else:
00420 node = self._aspace[method.MethodId]
00421 if node.call is None:
00422 res.StatusCode = ua.StatusCode(ua.StatusCodes.BadNothingToDo)
00423 else:
00424 try:
00425 res.OutputArguments = node.call(method.ObjectId, *method.InputArguments)
00426 for _ in method.InputArguments:
00427 res.InputArgumentResults.append(ua.StatusCode())
00428 except Exception:
00429 self.logger.exception("Error executing method call %s, an exception was raised: ", method)
00430 res.StatusCode = ua.StatusCode(ua.StatusCodes.BadUnexpectedError)
00431 return res
00432
00433
00434 class AddressSpace(object):
00435
00436 """
00437 The address space object stores all the nodes of the OPC-UA server
00438 and helper methods.
00439 The methods are thread safe
00440 """
00441
00442 def __init__(self):
00443 self.logger = logging.getLogger(__name__)
00444 self._nodes = {}
00445 self._lock = RLock()
00446 self._datachange_callback_counter = 200
00447 self._handle_to_attribute_map = {}
00448 self._default_idx = 2
00449 self._nodeid_counter = {0: 20000, 1: 2000}
00450
00451 def __getitem__(self, nodeid):
00452 with self._lock:
00453 if nodeid in self._nodes:
00454 return self._nodes.__getitem__(nodeid)
00455
00456 def __setitem__(self, nodeid, value):
00457 with self._lock:
00458 return self._nodes.__setitem__(nodeid, value)
00459
00460 def __contains__(self, nodeid):
00461 with self._lock:
00462 return self._nodes.__contains__(nodeid)
00463
00464 def __delitem__(self, nodeid):
00465 with self._lock:
00466 self._nodes.__delitem__(nodeid)
00467
00468 def generate_nodeid(self, idx=None):
00469 if idx is None:
00470 idx = self._default_idx
00471 if idx in self._nodeid_counter:
00472 self._nodeid_counter[idx] += 1
00473 else:
00474 self._nodeid_counter[idx] = 1
00475 nodeid = ua.NodeId(self._nodeid_counter[idx], idx)
00476 with self._lock:
00477 while True:
00478 if nodeid in self._nodes:
00479 nodeid = self.generate_nodeid(idx)
00480 else:
00481 return nodeid
00482
00483 def keys(self):
00484 with self._lock:
00485 return self._nodes.keys()
00486
00487 def empty(self):
00488 """
00489 Delete all nodes in address space
00490 """
00491 with self._lock:
00492 self._nodes = {}
00493
00494 def dump(self, path):
00495 """
00496 Dump address space as binary to file; note that server must be stopped for this method to work
00497 DO NOT DUMP AN ADDRESS SPACE WHICH IS USING A SHELF (load_aspace_shelf), ONLY CACHED NODES WILL GET DUMPED!
00498 """
00499
00500 for nodeid, ndata in self._nodes.items():
00501
00502 if ndata.call is not None:
00503 self._nodes[nodeid].call = None
00504
00505 with open(path, 'wb') as f:
00506 pickle.dump(self._nodes, f, pickle.HIGHEST_PROTOCOL)
00507
00508 def load(self, path):
00509 """
00510 Load address space from a binary file, overwriting everything in the current address space
00511 """
00512 with open(path, 'rb') as f:
00513 self._nodes = pickle.load(f)
00514
00515 def make_aspace_shelf(self, path):
00516 """
00517 Make a shelf for containing the nodes from the standard address space; this is typically only done on first
00518 start of the server. Subsequent server starts will load the shelf, nodes are then moved to a cache
00519 by the LazyLoadingDict class when they are accessed. Saving data back to the shelf
00520 is currently NOT supported, it is only used for the default OPC UA standard address space
00521
00522 Note: Intended for slow devices, such as Raspberry Pi, to greatly improve start up time
00523 """
00524 s = shelve.open(path, "n", protocol=pickle.HIGHEST_PROTOCOL)
00525 for nodeid, ndata in self._nodes.keys():
00526 s[nodeid.to_string()] = ndata
00527 s.close()
00528
00529 def load_aspace_shelf(self, path):
00530 """
00531 Load the standard address space nodes from a python shelve via LazyLoadingDict as needed.
00532 The dump() method can no longer be used if the address space is being loaded from a shelf
00533
00534 Note: Intended for slow devices, such as Raspberry Pi, to greatly improve start up time
00535 """
00536 class LazyLoadingDict(collections.MutableMapping):
00537 """
00538 Special dict that only loads nodes as they are accessed. If a node is accessed it gets copied from the
00539 shelve to the cache dict. All user nodes are saved in the cache ONLY. Saving data back to the shelf
00540 is currently NOT supported
00541 """
00542 def __init__(self, source):
00543 self.source = source
00544 self.cache = {}
00545
00546 def __getitem__(self, key):
00547
00548 try:
00549 return self.cache[key]
00550 except KeyError:
00551 node = self.cache[key] = self.source[key.to_string()]
00552 return node
00553
00554 def __setitem__(self, key, value):
00555
00556 self.cache[key] = value
00557
00558 def __contains__(self, key):
00559 return key in self.cache or key.to_string() in self.source
00560
00561 def __delitem__(self, key):
00562
00563 del self.cache[key]
00564
00565 def __iter__(self):
00566
00567 return iter(self.cache.keys())
00568
00569 def __len__(self):
00570
00571 return len(self.cache)
00572
00573 self._nodes = LazyLoadingDict(shelve.open(path, "r"))
00574
00575 def get_attribute_value(self, nodeid, attr):
00576 with self._lock:
00577 self.logger.debug("get attr val: %s %s", nodeid, attr)
00578 if nodeid not in self._nodes:
00579 dv = ua.DataValue()
00580 dv.StatusCode = ua.StatusCode(ua.StatusCodes.BadNodeIdUnknown)
00581 return dv
00582 node = self._nodes[nodeid]
00583 if attr not in node.attributes:
00584 dv = ua.DataValue()
00585 dv.StatusCode = ua.StatusCode(ua.StatusCodes.BadAttributeIdInvalid)
00586 return dv
00587 attval = node.attributes[attr]
00588 if attval.value_callback:
00589 return attval.value_callback()
00590 return attval.value
00591
00592 def set_attribute_value(self, nodeid, attr, value):
00593 with self._lock:
00594 self.logger.debug("set attr val: %s %s %s", nodeid, attr, value)
00595 if nodeid not in self._nodes:
00596 return ua.StatusCode(ua.StatusCodes.BadNodeIdUnknown)
00597 node = self._nodes[nodeid]
00598 if attr not in node.attributes:
00599 return ua.StatusCode(ua.StatusCodes.BadAttributeIdInvalid)
00600 if not value.SourceTimestamp:
00601 value.SourceTimestamp = datetime.utcnow()
00602 if not value.ServerTimestamp:
00603 value.ServerTimestamp = datetime.utcnow()
00604
00605 attval = node.attributes[attr]
00606 old = attval.value
00607 attval.value = value
00608 cbs = []
00609 if old.Value != value.Value:
00610 cbs = list(attval.datachange_callbacks.items())
00611
00612 for k, v in cbs:
00613 try:
00614 v(k, value)
00615 except Exception as ex:
00616 self.logger.exception("Error calling datachange callback %s, %s, %s", k, v, ex)
00617
00618 return ua.StatusCode()
00619
00620 def add_datachange_callback(self, nodeid, attr, callback):
00621 with self._lock:
00622 self.logger.debug("set attr callback: %s %s %s", nodeid, attr, callback)
00623 if nodeid not in self._nodes:
00624 return ua.StatusCode(ua.StatusCodes.BadNodeIdUnknown), 0
00625 node = self._nodes[nodeid]
00626 if attr not in node.attributes:
00627 return ua.StatusCode(ua.StatusCodes.BadAttributeIdInvalid), 0
00628 attval = node.attributes[attr]
00629 self._datachange_callback_counter += 1
00630 handle = self._datachange_callback_counter
00631 attval.datachange_callbacks[handle] = callback
00632 self._handle_to_attribute_map[handle] = (nodeid, attr)
00633 return ua.StatusCode(), handle
00634
00635 def delete_datachange_callback(self, handle):
00636 with self._lock:
00637 nodeid, attr = self._handle_to_attribute_map.pop(handle)
00638 self._nodes[nodeid].attributes[attr].datachange_callbacks.pop(handle)
00639
00640 def add_method_callback(self, methodid, callback):
00641 with self._lock:
00642 node = self._nodes[methodid]
00643 node.call = callback