ListData   A
last analyzed

Complexity

Total Complexity 1

Size/Duplication

Total Lines 6
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 6
rs 10
wmc 1

1 Method

Rating   Name   Duplication   Size   Complexity  
A __init__() 0 5 1
1
import logging
2
3
from PyQt5.QtCore import pyqtSignal, Qt, QObject, QSettings
4
from PyQt5.QtGui import QStandardItemModel, QStandardItem
5
from PyQt5.QtWidgets import QApplication, QMenu, QAction, QStyledItemDelegate, QComboBox, QVBoxLayout, QCheckBox, QDialog, QAbstractItemView
6
7
from opcua import ua
8
from opcua import Node
9
from opcua.common.ua_utils import string_to_val, val_to_string, data_type_to_string
10
11
from uawidgets.get_node_dialog import GetNodeButton
12
from uawidgets.utils import trycatchslot
13
14
15
logger = logging.getLogger(__name__)
16
17
18
class BitEditor(QDialog):
19
    """
20
    Edit bits in data
21
    FIXME: this should be a dialog but a Widget appearing directly in treewidget
22
    Patch welcome
23
    """
24
25
    def __init__(self, parent, attr, val):
26
        QDialog.__init__(self, parent)
27
        layout = QVBoxLayout(self)
28
        self.setLayout(layout)
29
        self.boxes = []
30
        self.enum = attr_to_enum(attr)
31
        for el in self.enum:
32
            box = QCheckBox(el.name, parent)
33
            layout.addWidget(box)
34
            self.boxes.append(box)
35
            if ua.ua_binary.test_bit(val, el.value):
36
                box.setChecked(True)
37
            else:
38
                box.setChecked(False)
39
40
    def get_byte(self):
41
        data = 0
42
        for box in self.boxes:
43
            if box.isChecked():
44
                data = ua.ua_binary.set_bit(data, self.enum[box.text()].value)
45
        return data
46
47
48
class _Data(object):
49
    def is_editable(self):
50
        if self.uatype != ua.VariantType.ExtensionObject:
51
            return True
52
        return False
53
54
55
class AttributeData(_Data):
56
    def __init__(self, attr, value, uatype):
57
        self.attr = attr
58
        self.value = value
59
        self.uatype = uatype
60
61
62
class MemberData(_Data):
63
    def __init__(self, obj, name, value, uatype):
64
        self.obj = obj
65
        self.name = name
66
        self.value = value
67
        self.uatype = uatype
68
69
70
class ListData(_Data):
71
    def __init__(self, mylist, idx, val, uatype):
72
        self.mylist = mylist
73
        self.idx = idx
74
        self.value = val
75
        self.uatype = uatype
76
77
78
class AttrsWidget(QObject):
79
80
    error = pyqtSignal(Exception)
81
    attr_written = pyqtSignal(ua.AttributeIds, ua.DataValue)
82
83
    def __init__(self, view, show_timestamps=True):
84
        QObject.__init__(self, view)
85
        self.view = view
86
        self._timestamps = show_timestamps
87
        delegate = MyDelegate(self.view, self)
88
        delegate.error.connect(self.error.emit)
89
        delegate.attr_written.connect(self.attr_written.emit)
90
        self.settings = QSettings()
91
        self.view.setItemDelegate(delegate)
92
        self.model = QStandardItemModel()
93
        self.model.setHorizontalHeaderLabels(['Attribute', 'Value', 'DataType'])
94
        state = self.settings.value("WindowState/attrs_widget_state", None)
95
        if state is not None:
96
            self.view.header().restoreState(state)
97
        self.view.setModel(self.model)
98
        self.current_node = None
99
        self.view.header().setSectionResizeMode(0)
100
        self.view.header().setStretchLastSection(True)
101
        self.view.expanded.connect(self._item_expanded)
102
        self.view.collapsed.connect(self._item_collapsed)
103
        self.view.setEditTriggers(QAbstractItemView.DoubleClicked)
104
105
        # Context menu
106
        self.view.setContextMenuPolicy(Qt.CustomContextMenu)
107
        self.view.customContextMenuRequested.connect(self.showContextMenu)
108
        copyaction = QAction("&Copy Value", self.model)
109
        copyaction.triggered.connect(self._copy_value)
110
        self._contextMenu = QMenu()
111
        self._contextMenu.addAction(copyaction)
112
113
    def save_state(self):
114
        self.settings.setValue("WindowState/attrs_widget_state", self.view.header().saveState())
115
116
    def _item_expanded(self, idx):
117
        if not idx.parent().isValid():
118
            # only for value attributes which a re childs
119
            # maybe add more tests
120
            return
121
        it = self.model.itemFromIndex(idx.sibling(0, 1))
122
        it.setText("")
123
124
    def _item_collapsed(self, idx):
125
        it = self.model.itemFromIndex(idx.sibling(0, 1))
126
        data = it.data(Qt.UserRole)
127
        it.setText(val_to_string(data.value))
128
129
    def showContextMenu(self, position):
130
        item = self.get_current_item()
131
        if item:
132
            self._contextMenu.exec_(self.view.viewport().mapToGlobal(position))
133
134
    def get_current_item(self, col_idx=0):
135
        idx = self.view.currentIndex()
136
        return self.model.item(idx.row(), col_idx)
137
138
    def _copy_value(self, position):
139
        it = self.get_current_item(1)
140
        if it:
141
            QApplication.clipboard().setText(it.text())
142
143
    def clear(self):
144
        # remove all rows but not header!!
145
        self.model.removeRows(0, self.model.rowCount())
146
147
    def reload(self):
148
        self.show_attrs(self.current_node)
149
150
    def show_attrs(self, node):
151
        self.current_node = node
152
        self.clear()
153
        if self.current_node:
154
            self._show_attrs()
155
        self.view.expandToDepth(0)
156
157
    def _show_attrs(self):
158
        attrs = self.get_all_attrs()
159
        for attr, dv in attrs:
160
            try:
161
                # try/except to show as many attributes as possible
162
                if attr == ua.AttributeIds.Value:
163
                    self._show_value_attr(attr, dv)
164
                else:
165
                    self._show_attr(attr, dv)
166
            except Exception as ex:
167
                logger.exception("Exception while displaying attribute %s with value %s for node %s", attr, dv, self.current_node)
168
                self.error.emit(ex)
169
170
    def _show_attr(self, attr, dv):
171
        if attr == ua.AttributeIds.DataType:
172
            # FIXME: Could query for browsename here, it does not cost much
173
            string = data_type_to_string(dv.Value.Value)
174
        elif attr in (ua.AttributeIds.AccessLevel,
175
                      ua.AttributeIds.UserAccessLevel,
176
                      ua.AttributeIds.WriteMask,
177
                      ua.AttributeIds.UserWriteMask,
178
                      ua.AttributeIds.EventNotifier):
179
            string = enum_to_string(attr, dv.Value.Value)
180
        else:
181
            string = val_to_string(dv.Value.Value)
182
        name_item = QStandardItem(attr.name)
183
        vitem = QStandardItem(string)
184
        vitem.setData(AttributeData(attr, dv.Value.Value, dv.Value.VariantType), Qt.UserRole)
185
        self.model.appendRow([name_item, vitem, QStandardItem(dv.Value.VariantType.name)])
186
187
    def _show_value_attr(self, attr, dv):
188
        name_item = QStandardItem("Value")
189
        vitem = QStandardItem()
190
        items = self._show_val(name_item, None, "Value", dv.Value.Value, dv.Value.VariantType)
191
        items[1].setData(AttributeData(attr, dv.Value.Value, dv.Value.VariantType), Qt.UserRole)
192
        row = [name_item, vitem, QStandardItem(dv.Value.VariantType.name)]
193
        self.model.appendRow(row)
194
        self._show_timestamps(name_item, dv)
195
196
    def _show_val(self, parent, obj, name, val, vtype):
197
        name_item = QStandardItem(name)
198
        vitem = QStandardItem()
199
        vitem.setText(val_to_string(val))
200
        vitem.setData(MemberData(obj, name, val, vtype), Qt.UserRole)
201
        row = [name_item, vitem, QStandardItem(vtype.name)]
202
        # if we have a list or extension object we display children
203
        if isinstance(val, list):
204
            row[2].setText("List of " + vtype.name)
205
            self._show_list(name_item, val, vtype)
206
        elif vtype == ua.VariantType.ExtensionObject:
207
            self._show_ext_obj(name_item, val)
208
        parent.appendRow(row)
209
        return row
210
211
    def _show_list(self, parent, mylist, vtype):
212
        for idx, val in enumerate(mylist):
213
            name_item = QStandardItem(str(idx))
214
            vitem = QStandardItem()
215
            vitem.setText(val_to_string(val))
216
            vitem.setData(ListData(mylist, idx, val, vtype), Qt.UserRole)
217
            row = [name_item, vitem, QStandardItem(vtype.name)]
218
            parent.appendRow(row)
219
            if vtype == ua.VariantType.ExtensionObject:
220
                self._show_ext_obj(name_item, val)
221
    
222
    def refresh_list(self, parent, mylist, vtype):
223
        while parent.hasChildren():
224
            self.model.removeRow(0, parent.index())
225
        self._show_list(parent, mylist, vtype)
226
227
    def _show_ext_obj(self, item, val):
228
        item.setText(item.text() + ": " + val.__class__.__name__)
229
        for att_name, att_type in val.ua_types:
230
            member_val = getattr(val, att_name)
231
            if att_type.startswith("ListOf"):
232
                att_type = att_type[6:]
233
            attr = getattr(ua.VariantType, att_type)
234
            self._show_val(item, val, att_name, member_val, attr)
235
236
    def _show_timestamps(self, item, dv):
237
        #while item.hasChildren():
238
            #self.model.removeRow(0, item.index())
239
        string = val_to_string(dv.ServerTimestamp)
240
        item.appendRow([QStandardItem("Server Timestamp"), QStandardItem(string), QStandardItem(ua.VariantType.DateTime.name)])
241
        string = val_to_string(dv.SourceTimestamp)
242
        item.appendRow([QStandardItem("Source Timestamp"), QStandardItem(string), QStandardItem(ua.VariantType.DateTime.name)])
243
244
245
    def get_all_attrs(self):
246
        attrs = [attr for attr in ua.AttributeIds]
247
        dvs = self.current_node.get_attributes(attrs)
248
        res = []
249
        for idx, dv in enumerate(dvs):
250
            if dv.StatusCode.is_good():
251
                res.append((attrs[idx], dv))
252
        res.sort(key=lambda x: x[0].name)
253
        return res
254
255
256
class MyDelegate(QStyledItemDelegate):
257
258
    error = pyqtSignal(Exception)
259
    attr_written = pyqtSignal(ua.AttributeIds, ua.DataValue)
260
261
    def __init__(self, parent, attrs_widget):
262
        QStyledItemDelegate.__init__(self, parent)
263
        self.attrs_widget = attrs_widget
264
265
    @trycatchslot
266
    def createEditor(self, parent, option, idx):
267
        if idx.column() != 1:
268
            return None
269
        item = self.attrs_widget.model.itemFromIndex(idx)
270
        data = item.data(Qt.UserRole)
271
        if not data.is_editable():
272
            return None
273
        text = item.text()
274
        if isinstance(data, (ListData, MemberData)):
275
            return QStyledItemDelegate.createEditor(self, parent, option, idx)
276
        elif data.attr == ua.AttributeIds.NodeId:
277
            return None
278
        elif data.uatype == ua.VariantType.Boolean:
279
            combo = QComboBox(parent)
280
            combo.addItem("True")
281
            combo.addItem("False")
282
            combo.setCurrentText(text)
283
            return combo
284
        elif data.attr == ua.AttributeIds.NodeClass:
285
            combo = QComboBox(parent)
286
            for nclass in ua.NodeClass:
287
                combo.addItem(nclass.name)
288
            combo.setCurrentText(text)
289
            return combo
290
        elif data.attr == ua.AttributeIds.ValueRank:
291
            combo = QComboBox(parent)
292
            for rank in ua.ValueRank:
293
                combo.addItem(rank.name)
294
            combo.setCurrentText(text)
295
            return combo
296
        elif data.attr == ua.AttributeIds.DataType:
297
            #nodeid = getattr(ua.ObjectIds, text)
298
            nodeid = data.value
299
            node = Node(self.attrs_widget.current_node.server, nodeid)
300
            startnode = Node(self.attrs_widget.current_node.server, ua.ObjectIds.BaseDataType)
301
            button = GetNodeButton(parent, node, startnode)
302
            return button
303
        elif data.attr in (ua.AttributeIds.AccessLevel,
304
                           ua.AttributeIds.UserAccessLevel,
305
                           ua.AttributeIds.WriteMask,
306
                           ua.AttributeIds.UserWriteMask,
307
                           ua.AttributeIds.EventNotifier):
308
            return BitEditor(parent, data.attr, data.value)
309
        else:
310
            return QStyledItemDelegate.createEditor(self, parent, option, idx)
311
312
    #def setEditorData(self, editor, index):
313
        #pass
314
315
    @trycatchslot
316
    def setModelData(self, editor, model, idx):
317
        # if user is setting a value on a null variant, try using the nodes datatype instead
318
        data = model.data(idx, Qt.UserRole)
319
320
        if isinstance(data, AttributeData):
321
            self._set_attribute_data(data, editor, model, idx)
322
        elif isinstance(data, MemberData):
323
            self._set_member_data(data, editor, model, idx)
324
        elif isinstance(data, ListData):
325
            self._set_list_data(data, editor, model, idx)
326
        else:
327
            logger.info("Error while setting model data, data is %s", data)
328
329
    def _set_list_data(self, data, editor, model, idx):
330
        text = editor.text()
331
        data.mylist[data.idx] = string_to_val(text, data.uatype)
332
        model.setItemData(idx, {Qt.DisplayRole: text, Qt.UserRole: data})
333
        attr_data = self._get_attr_data(idx, model)
334
        self._write_attr(attr_data)
335
336
    def _set_member_data(self, data, editor, model, idx):
337
        val = string_to_val(editor.text(), data.uatype)
338
        data.value = val
339
        model.setItemData(idx, {Qt.DisplayRole: editor.text(), Qt.UserRole: data})
340
        setattr(data.obj, data.name, val)
341
        attr_data = self._get_attr_data(idx, model)
342
        self._write_attr(attr_data)
343
344
    def _get_attr_data(self, idx, model):
345
        while True:
346
            idx = idx.parent()
347
            it = model.itemFromIndex(idx.sibling(0, 1))
348
            data = it.data(Qt.UserRole)
349
            if isinstance(data, AttributeData):
350
                return data
351
352
    def _get_parent_data(self, idx, model):
353
        parent_idx = idx.parent()
354
        it = model.itemFromIndex(parent_idx.sibling(0, 1))
355
        return parent_idx, it.data(Qt.UserRole)
356
357
    def _set_attribute_data(self, data, editor, model, idx):
358
        if data.attr is ua.AttributeIds.Value:
359
            #for value we checkd data type from the variable data type
360
            # this is more robust
361
            try:
362
                data.uatype = self.attrs_widget.current_node.get_data_type_as_variant_type()
363
            except Exception as ex:
364
                logger.exception("Could get primitive type of variable %s", self.attrs_widget.current_node)
365
                self.error.emit(ex)
366
                raise
367
368
        if data.attr == ua.AttributeIds.NodeClass:
369
            data.value = ua.NodeClass[editor.currentText()]
370
            text = editor.currentText()
371
        elif data.attr == ua.AttributeIds.ValueRank:
372
            data.value = ua.ValueRank[editor.currentText()]
373
            text = editor.currentText()
374
        elif data.attr == ua.AttributeIds.DataType:
375
            data.value = editor.get_node().nodeid
376
            text = data_type_to_string(data.value)
377
        elif data.attr in (ua.AttributeIds.AccessLevel,
378
                           ua.AttributeIds.UserAccessLevel,
379
                           ua.AttributeIds.WriteMask,
380
                           ua.AttributeIds.UserWriteMask,
381
                           ua.AttributeIds.EventNotifier):
382
            data.value = editor.get_byte()
383
            text = enum_to_string(data.attr, data.value)
384
        else:
385
            if isinstance(editor, QComboBox):
386
                text = editor.currentText()
387
            else:
388
                text = editor.text()
389
            data.value = string_to_val(text, data.uatype)
390
        model.setItemData(idx, {Qt.DisplayRole: text, Qt.UserRole: data})
391
        self._write_attr(data)
392
        if isinstance(data.value, list):
393
            # we need to refresh children
394
            item = self.attrs_widget.model.itemFromIndex(idx.sibling(0, 0))
395
            self.attrs_widget.refresh_list(item, data.value, data.uatype)
396
397
    def _write_attr(self, data):
398
        dv = ua.DataValue(ua.Variant(data.value, varianttype=data.uatype))
399
        try:
400
            logger.info("Writing attribute %s of node %s with value: %s", data.attr, self.attrs_widget.current_node, dv)
401
            self.attrs_widget.current_node.set_attribute(data.attr, dv)
402
        except Exception as ex:
403
            logger.exception("Exception while writing %s to %s", dv, data.attr)
404
            self.error.emit(ex)
405
        else:
406
            self.attr_written.emit(data.attr, dv)
407
408
409
def attr_to_enum(attr):
410
    attr_name = attr.name
411
    if attr_name.startswith("User"):
412
        attr_name = attr_name[4:]
413
    return getattr(ua, attr_name)
414
415
416
def enum_to_string(attr, val):
417
    attr_enum = attr_to_enum(attr)
418
    string = ", ".join([e.name for e in attr_enum.parse_bitfield(val)])
419
    return string
420