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.NodesToDelete: |
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 = {0: 20000, 1: 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=None): |
467
|
1 |
|
if idx is None: |
468
|
1 |
|
idx = self._default_idx |
469
|
1 |
|
if idx in self._nodeid_counter: |
470
|
|
|
self._nodeid_counter[idx] += 1 |
471
|
1 |
|
else: |
472
|
1 |
|
self._nodeid_counter[idx] = 1 |
473
|
1 |
|
nodeid = ua.NodeId(self._nodeid_counter[idx], idx) |
474
|
|
|
with self._lock: # OK since reentrant lock |
475
|
1 |
|
while True: |
476
|
|
|
if nodeid in self._nodes: |
477
|
|
|
nodeid = self.generate_nodeid(idx) |
478
|
|
|
else: |
479
|
|
|
return nodeid |
480
|
|
|
|
481
|
|
|
def keys(self): |
482
|
|
|
with self._lock: |
483
|
|
|
return self._nodes.keys() |
484
|
1 |
|
|
485
|
|
|
def empty(self): |
486
|
|
|
""" |
487
|
|
|
Delete all nodes in address space |
488
|
|
|
""" |
489
|
|
|
with self._lock: |
490
|
|
|
self._nodes = {} |
491
|
|
|
|
492
|
|
|
def dump(self, path): |
493
|
|
|
""" |
494
|
|
|
dump address space as binary to file |
495
|
|
|
""" |
496
|
|
|
s = shelve.open(path, "n", protocol = pickle.HIGHEST_PROTOCOL) |
497
|
|
|
for nodeid in self._nodes.keys(): |
498
|
|
|
s[nodeid.to_string()] = self._nodes[nodeid] |
499
|
|
|
s.close() |
500
|
|
|
|
501
|
|
|
def load(self, path): |
502
|
|
|
""" |
503
|
|
|
load address space from file, overwritting everything current address space |
504
|
|
|
""" |
505
|
|
|
class LazyLoadingDict(collections.MutableMapping): |
506
|
|
|
def __init__(self, source): |
507
|
|
|
self.source = source |
508
|
|
|
self.cache = {} |
509
|
|
|
|
510
|
|
|
def __getitem__(self, key): |
511
|
|
|
try: |
512
|
|
|
return self.cache[key] |
513
|
|
|
except KeyError: |
514
|
|
|
node = self.cache[key] = self.source[key.to_string()] |
515
|
|
|
return node |
516
|
|
|
|
517
|
1 |
|
def __setitem__(self, key, value): |
518
|
1 |
|
self.cache[key] = value |
519
|
1 |
|
|
520
|
1 |
|
def __contains__(self, key): |
521
|
1 |
|
return key in self.cache or key.to_string() in self.source |
522
|
1 |
|
|
523
|
1 |
|
def __delitem__(self, key): |
524
|
1 |
|
raise NotImplementedError |
525
|
1 |
|
|
526
|
1 |
|
def __iter__(self): |
527
|
1 |
|
raise NotImplementedError |
528
|
1 |
|
|
529
|
1 |
|
def __len__(self): |
530
|
1 |
|
raise NotImplementedError |
531
|
|
|
|
532
|
1 |
|
self._nodes = LazyLoadingDict(shelve.open(path, "r")) |
533
|
|
|
|
534
|
1 |
|
def get_attribute_value(self, nodeid, attr): |
535
|
1 |
|
with self._lock: |
536
|
1 |
|
self.logger.debug("get attr val: %s %s", nodeid, attr) |
537
|
1 |
|
if nodeid not in self._nodes: |
538
|
1 |
|
dv = ua.DataValue() |
539
|
1 |
|
dv.StatusCode = ua.StatusCode(ua.StatusCodes.BadNodeIdUnknown) |
540
|
1 |
|
return dv |
541
|
1 |
|
node = self._nodes[nodeid] |
542
|
1 |
|
if attr not in node.attributes: |
543
|
1 |
|
dv = ua.DataValue() |
544
|
1 |
|
dv.StatusCode = ua.StatusCode(ua.StatusCodes.BadAttributeIdInvalid) |
545
|
1 |
|
return dv |
546
|
|
|
attval = node.attributes[attr] |
547
|
1 |
|
if attval.value_callback: |
548
|
1 |
|
return attval.value_callback() |
549
|
1 |
|
return attval.value |
550
|
1 |
|
|
551
|
1 |
|
def set_attribute_value(self, nodeid, attr, value): |
552
|
1 |
|
with self._lock: |
553
|
|
|
self.logger.debug("set attr val: %s %s %s", nodeid, attr, value) |
554
|
1 |
|
if nodeid not in self._nodes: |
555
|
1 |
|
return ua.StatusCode(ua.StatusCodes.BadNodeIdUnknown) |
556
|
1 |
|
node = self._nodes[nodeid] |
557
|
|
|
if attr not in node.attributes: |
558
|
|
|
return ua.StatusCode(ua.StatusCodes.BadAttributeIdInvalid) |
559
|
|
|
if not value.SourceTimestamp: |
560
|
1 |
|
value.SourceTimestamp = datetime.utcnow() |
561
|
|
|
if not value.ServerTimestamp: |
562
|
1 |
|
value.ServerTimestamp = datetime.utcnow() |
563
|
1 |
|
|
564
|
1 |
|
attval = node.attributes[attr] |
565
|
1 |
|
old = attval.value |
566
|
|
|
attval.value = value |
567
|
1 |
|
cbs = [] |
568
|
1 |
|
if old.Value != value.Value: # only send call callback when a value change has happend |
569
|
1 |
|
cbs = list(attval.datachange_callbacks.items()) |
570
|
1 |
|
|
571
|
1 |
|
for k, v in cbs: |
572
|
1 |
|
try: |
573
|
1 |
|
v(k, value) |
574
|
1 |
|
except Exception as ex: |
575
|
1 |
|
self.logger.exception("Error calling datachange callback %s, %s, %s", k, v, ex) |
576
|
|
|
|
577
|
1 |
|
return ua.StatusCode() |
578
|
1 |
|
|
579
|
1 |
|
def add_datachange_callback(self, nodeid, attr, callback): |
580
|
1 |
|
with self._lock: |
581
|
|
|
self.logger.debug("set attr callback: %s %s %s", nodeid, attr, callback) |
582
|
1 |
|
if nodeid not in self._nodes: |
583
|
1 |
|
return ua.StatusCode(ua.StatusCodes.BadNodeIdUnknown), 0 |
584
|
1 |
|
node = self._nodes[nodeid] |
585
|
1 |
|
if attr not in node.attributes: |
586
|
|
|
return ua.StatusCode(ua.StatusCodes.BadAttributeIdInvalid), 0 |
587
|
|
|
attval = node.attributes[attr] |
588
|
|
|
self._datachange_callback_counter += 1 |
589
|
|
|
handle = self._datachange_callback_counter |
590
|
|
|
attval.datachange_callbacks[handle] = callback |
591
|
|
|
self._handle_to_attribute_map[handle] = (nodeid, attr) |
592
|
|
|
return ua.StatusCode(), handle |
593
|
|
|
|
594
|
|
|
def delete_datachange_callback(self, handle): |
595
|
|
|
with self._lock: |
596
|
|
|
nodeid, attr = self._handle_to_attribute_map.pop(handle) |
597
|
|
|
self._nodes[nodeid].attributes[attr].datachange_callbacks.pop(handle) |
598
|
|
|
|
599
|
|
|
def add_method_callback(self, methodid, callback): |
600
|
|
|
with self._lock: |
601
|
|
|
node = self._nodes[methodid] |
602
|
|
|
node.call = callback |
603
|
|
|
|