Passed
Push — dev ( 4b39c9...95c1a8 )
by Olivier
04:21
created

opcua.NodeData.__init__()   A

Complexity

Conditions 1

Size

Total Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

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