Completed
Push — master ( 31ba23...26ea6b )
by Olivier
01:10
created

DataChangeUI.canDropMimeData()   A

Complexity

Conditions 1

Size

Total Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
dl 0
loc 2
rs 10
c 0
b 0
f 0
1
#! /usr/bin/env python3
2
3
import sys
4
from datetime import datetime
5
from enum import Enum
6
7
from PyQt5.QtCore import pyqtSignal, QTimer, Qt, QObject, QSettings, QModelIndex, QMimeData
8
from PyQt5.QtGui import QStandardItemModel, QStandardItem, QIcon
9
from PyQt5.QtWidgets import QMainWindow, QWidget, QApplication, QAbstractItemView, QMenu, QAction
10
11
from opcua import ua
12
from opcua.common.ua_utils import string_to_variant, variant_to_string, val_to_string
13
14
from freeopcuaclient.uaclient import UaClient
15
from freeopcuaclient.mainwindow_ui import Ui_MainWindow
16
from freeopcuaclient import resources
17
18
19
class DataChangeHandler(QObject):
20
    data_change_fired = pyqtSignal(object, str, str)
21
22
    def datachange_notification(self, node, val, data):
23
        if data.monitored_item.Value.SourceTimestamp:
24
            dato = data.monitored_item.Value.SourceTimestamp.isoformat()
25
        elif data.monitored_item.Value.ServerTimestamp:
26
            dato = data.monitored_item.Value.ServerTimestamp.isoformat()
27
        else:
28
            dato = datetime.now().isoformat()
29
        self.data_change_fired.emit(node, str(val), dato)
30
31
32
class EventHandler(QObject):
33
    event_fired = pyqtSignal(object)
34
35
    def event_notification(self, event):
36
        self.event_fired.emit(event)
37
38
39
class EventUI(object):
40
41
    def __init__(self, window, uaclient):
42
        self.window = window
43
        self.uaclient = uaclient
44
        self._handler = EventHandler()
45
        self._subscribed_nodes = []  # FIXME: not really needed
46
        self.model = QStandardItemModel()
47
        self.window.ui.evView.setModel(self.model)
48
        self.window.ui.actionSubscribeEvent.triggered.connect(self._subscribe)
49
        self.window.ui.actionUnsubscribeEvents.triggered.connect(self._unsubscribe)
50
        # context menu
51
        self.window.ui.treeView.addAction(self.window.ui.actionSubscribeEvent)
52
        self.window.ui.treeView.addAction(self.window.ui.actionUnsubscribeEvents)
53
        self._handler.event_fired.connect(self._update_event_model, type=Qt.QueuedConnection)
54
55
    def clear(self):
56
        self._subscribed_nodes = []
57
        self.model.clear()
58
59
    def _subscribe(self):
60
        node = self.window.get_current_node()
61
        if node is None:
62
            return
63
        if node in self._subscribed_nodes:
64
            print("allready subscribed to event for node: ", node)
65
            return
66
        self.window.ui.evDockWidget.raise_()
67
        try:
68
            self.uaclient.subscribe_events(node, self._handler)
69
        except Exception as ex:
70
            self.window.show_error(ex)
71
            raise
72
        else:
73
            self._subscribed_nodes.append(node)
74
75
    def _unsubscribe(self):
76
        node = self.window.get_current_node()
77
        if node is None:
78
            return
79
        self._subscribed_nodes.remove(node)
80
        self.uaclient.unsubscribe_events(node)
81
82
    def _update_event_model(self, event):
83
        self.model.appendRow([QStandardItem(str(event))])
84
85
86
class DataChangeUI(object):
87
88
    def __init__(self, window, uaclient):
89
        self.window = window
90
        self.uaclient = uaclient
91
        self._subhandler = DataChangeHandler()
92
        self._subscribed_nodes = []
93
        self.model = QStandardItemModel()
94
        self.window.ui.subView.setModel(self.model)
95
        self.window.ui.subView.horizontalHeader().setSectionResizeMode(1)
96
97
        self.window.ui.actionSubscribeDataChange.triggered.connect(self._subscribe)
98
        self.window.ui.actionUnsubscribeDataChange.triggered.connect(self._unsubscribe)
99
100
        # populate contextual menu
101
        self.window.ui.treeView.addAction(self.window.ui.actionSubscribeDataChange)
102
        self.window.ui.treeView.addAction(self.window.ui.actionUnsubscribeDataChange)
103
104
        # handle subscriptions
105
        self._subhandler.data_change_fired.connect(self._update_subscription_model, type=Qt.QueuedConnection)
106
        
107
        # accept drops
108
        self.model.canDropMimeData = self.canDropMimeData
109
        self.model.dropMimeData = self.dropMimeData
110
111
    def canDropMimeData(self, mdata, action, row, column, parent):
112
        return True
113
114
    def dropMimeData(self, mdata, action, row, column, parent):
115
        node = self.uaclient.client.get_node(mdata.text())
116
        self._subscribe(node)
117
        return True
118
119
    def clear(self):
120
        self._subscribed_nodes = []
121
        self.model.clear()
122
123
    def _subscribe(self, node=None):
124
        if node is None:
125
            node = self.window.get_current_node()
126
            if node is None:
127
                return
128
        if node in self._subscribed_nodes:
129
            print("allready subscribed to node: ", node)
130
            return
131
        self.model.setHorizontalHeaderLabels(["DisplayName", "Value", "Timestamp"])
132
        text = str(node.get_display_name().Text)
133
        row = [QStandardItem(text), QStandardItem("No Data yet"), QStandardItem("")]
134
        row[0].setData(node)
135
        self.model.appendRow(row)
136
        self._subscribed_nodes.append(node)
137
        self.window.ui.subDockWidget.raise_()
138
        try:
139
            self.uaclient.subscribe_datachange(node, self._subhandler)
140
        except Exception as ex:
141
            self.window.show_error(ex)
142
            idx = self.model.indexFromItem(row[0])
143
            self.model.takeRow(idx.row())
144
            raise
145
146
    def _unsubscribe(self):
147
        node = self.window.get_current_node()
148
        if node is None:
149
            return
150
        self.uaclient.unsubscribe_datachange(node)
151
        self._subscribed_nodes.remove(node)
152
        i = 0
153
        while self.model.item(i):
154
            item = self.model.item(i)
155
            if item.data() == node:
156
                self.model.removeRow(i)
157
            i += 1
158
159
    def _update_subscription_model(self, node, value, timestamp):
160
        i = 0
161
        while self.model.item(i):
162
            item = self.model.item(i)
163
            if item.data() == node:
164
                it = self.model.item(i, 1)
165
                it.setText(value)
166
                it_ts = self.model.item(i, 2)
167
                it_ts.setText(timestamp)
168
            i += 1
169
170
171
class AttrsUI(object):
172
173
    def __init__(self, window, uaclient):
174
        self.window = window
175
        self.uaclient = uaclient
176
        self.model = QStandardItemModel()
177
        self.window.ui.attrView.setModel(self.model)
178
        self.window.ui.attrView.doubleClicked.connect(self.edit_attr)
179
        self.model.itemChanged.connect(self.edit_attr_finished)
180
        self.window.ui.attrView.header().setSectionResizeMode(1)
181
182
        self.window.ui.treeView.activated.connect(self.show_attrs)
183
        self.window.ui.treeView.clicked.connect(self.show_attrs)
184
        self.window.ui.attrRefreshButton.clicked.connect(self.show_attrs)
185
186
        # Context menu
187
        self.window.ui.attrView.setContextMenuPolicy(Qt.CustomContextMenu)
188
        self.window.ui.attrView.customContextMenuRequested.connect(self.showContextMenu)
189
        copyaction = QAction("&Copy Value", self.model)
190
        copyaction.triggered.connect(self._copy_value)
191
        self._contextMenu = QMenu()
192
        self._contextMenu.addAction(copyaction)
193
194
    def _check_edit(self, item):
195
        """
196
        filter only element we want to edit.
197
        take either idx eller item as argument
198
        """
199
        if item.column() != 1:
200
            return False
201
        name_item = self.model.item(item.row(), 0)
202
        if name_item.text() != "Value":
203
            return False
204
        return True
205
206
    def edit_attr(self, idx):
207
        if not self._check_edit(idx):
208
            return
209
        attritem = self.model.item(idx.row(), 0)
210
        if attritem.text() == "Value":
211
            self.window.ui.attrView.edit(idx)
212
213
    def edit_attr_finished(self, item):
214
        if not self._check_edit(item):
215
            return
216
        try:
217
            var = item.data()
218
            val = item.text()
219
            var = string_to_variant(val, var.VariantType)
220
            self.current_node.set_value(var)
221
        except Exception as ex:
222
            self.window.show_error(ex)
223
            raise
224
        finally:
225
            dv = self.current_node.get_data_value()
226
            item.setText(variant_to_string(dv.Value))
227
            name_item = self.model.item(item.row(), 0)
228
            name_item.child(0, 1).setText(val_to_string(dv.ServerTimestamp))
229
            name_item.child(1, 1).setText(val_to_string(dv.SourceTimestamp))
230
231
    def showContextMenu(self, position):
232
        item = self.get_current_item()
233
        if item:
234
            self._contextMenu.exec_(self.window.ui.attrView.mapToGlobal(position))
235
236
    def get_current_item(self, col_idx=0):
237
        idx = self.window.ui.attrView.currentIndex()
238
        return self.model.item(idx.row(), col_idx)
239
240
    def _copy_value(self, position):
241
        it = self.get_current_item(1)
242
        if it:
243
            QApplication.clipboard().setText(it.text())
244
245
    def clear(self):
246
        self.model.clear()
247
248
    def show_attrs(self, idx):
249
        if not isinstance(idx, QModelIndex):
250
            idx = None
251
        self.current_node = self.window.get_current_node(idx)
252
        self.model.clear()
253
        if self.current_node:
254
            self._show_attrs(self.current_node)
255
        self.window.ui.attrView.expandAll()
256
257
    def _show_attrs(self, node):
258
        try:
259
            attrs = self.uaclient.get_all_attrs(node)
260
        except Exception as ex:
261
            self.window.show_error(ex)
262
            raise
263
        self.model.setHorizontalHeaderLabels(['Attribute', 'Value', 'DataType'])
264
        for name, dv in attrs:
265
            if name == "DataType":
266
                if isinstance(dv.Value.Value.Identifier, int) and dv.Value.Value.Identifier < 63:
267
                    string = ua.DataType_to_VariantType(dv.Value.Value).name
268
                elif dv.Value.Value.Identifier in ua.ObjectIdNames:
269
                    string = ua.ObjectIdNames[dv.Value.Value.Identifier]
270
                else:
271
                    string = dv.Value.Value.to_string()
272
            elif name in ("AccessLevel", "UserAccessLevel"):
273
                string = ",".join([e.name for e in ua.int_to_AccessLevel(dv.Value.Value)])
274
            elif name in ("WriteMask", "UserWriteMask"):
275
                string = ",".join([e.name for e in ua.int_to_WriteMask(dv.Value.Value)])
276
            elif name in ("EventNotifier"):
277
                string = ",".join([e.name for e in ua.int_to_EventNotifier(dv.Value.Value)])
278
            else:
279
                string = variant_to_string(dv.Value)
280
            name_item = QStandardItem(name)
281
            vitem = QStandardItem(string)
282
            vitem.setData(dv.Value)
283
            self.model.appendRow([name_item, vitem, QStandardItem(dv.Value.VariantType.name)])
284
            if name == "Value":
285
                string = val_to_string(dv.ServerTimestamp)
286
                name_item.appendRow([QStandardItem("Server Timestamp"), QStandardItem(string), QStandardItem(ua.VariantType.DateTime.name)])
287
                string = val_to_string(dv.SourceTimestamp)
288
                name_item.appendRow([QStandardItem("Source Timestamp"), QStandardItem(string), QStandardItem(ua.VariantType.DateTime.name)])
289
    
290
291
292
class RefsUI(object):
293
294
    def __init__(self, window, uaclient):
295
        self.window = window
296
        self.uaclient = uaclient
297
        self.model = QStandardItemModel()
298
        self.window.ui.refView.setModel(self.model)
299
        self.window.ui.refView.horizontalHeader().setSectionResizeMode(1)
300
301
        self.window.ui.treeView.activated.connect(self.show_refs)
302
        self.window.ui.treeView.clicked.connect(self.show_refs)
303
304
    def clear(self):
305
        self.model.clear()
306
307
    def show_refs(self, idx):
308
        node = self.window.get_current_node(idx)
309
        self.model.clear()
310
        if node:
311
            self._show_refs(node)
312
313
    def _show_refs(self, node):
314
        self.model.setHorizontalHeaderLabels(['ReferenceType', 'NodeId', "BrowseName", "TypeDefinition"])
315
        try:
316
            refs = self.uaclient.get_all_refs(node)
317
        except Exception as ex:
318
            self.window.show_error(ex)
319
            raise
320
        for ref in refs:
321
            typename = ua.ObjectIdNames[ref.ReferenceTypeId.Identifier]
322
            if ref.NodeId.NamespaceIndex == 0 and ref.NodeId.Identifier in ua.ObjectIdNames:
323
                nodeid = ua.ObjectIdNames[ref.NodeId.Identifier]
324
            else:
325
                nodeid = ref.NodeId.to_string()
326
            if ref.TypeDefinition.Identifier in ua.ObjectIdNames:
327
                typedef = ua.ObjectIdNames[ref.TypeDefinition.Identifier]
328
            else:
329
                typedef = ref.TypeDefinition.to_string()
330
            self.model.appendRow([QStandardItem(typename),
331
                                  QStandardItem(nodeid),
332
                                  QStandardItem(ref.BrowseName.to_string()),
333
                                  QStandardItem(typedef)
334
                                  ])
335
336
337
class TreeUI(object):
338
339
    def __init__(self, window, uaclient):
340
        self.window = window
341
        self.uaclient = uaclient
342
        self.model = TreeViewModel(self.uaclient)
343
        self.model.clear()  # FIXME: do we need this?
344
        self.model.error.connect(self.window.show_error)
345
        self.window.ui.treeView.setModel(self.model)
346
        #self.window.ui.treeView.setUniformRowHeights(True)
347
        self.window.ui.treeView.setSelectionBehavior(QAbstractItemView.SelectRows)
348
        self.window.ui.treeView.header().setSectionResizeMode(1)
349
        self.window.ui.actionCopyPath.triggered.connect(self._copy_path)
350
        self.window.ui.actionCopyNodeId.triggered.connect(self._copy_nodeid)
351
        # add items to context menu
352
        self.window.ui.treeView.addAction(self.window.ui.actionCopyPath)
353
        self.window.ui.treeView.addAction(self.window.ui.actionCopyNodeId)
354
355
    def clear(self):
356
        self.model.clear()
357
358
    def start(self):
359
        self.model.clear()
360
        self.model.add_item(*self.uaclient.get_root_node_and_desc())
361
362
    def _copy_path(self):
363
        path = self.get_current_path()
364
        path = ",".join(path)
365
        QApplication.clipboard().setText(path)
366
367
    def _copy_nodeid(self):
368
        node = self.get_current_node()
369
        if node:
370
            text = node.nodeid.to_string()
371
        else:
372
            text = ""
373
        QApplication.clipboard().setText(text)
374
375
    def get_current_path(self):
376
        idx = self.window.ui.treeView.currentIndex()
377
        idx = idx.sibling(idx.row(), 0)
378
        it = self.model.itemFromIndex(idx)
379
        path = []
380
        while it and it.data():
381
            node = it.data()
382
            name = node.get_browse_name().to_string()
383
            path.insert(0, name)
384
            it = it.parent()
385
        return path
386
387
    def get_current_node(self, idx=None):
388
        if idx is None:
389
            idx = self.window.ui.treeView.currentIndex()
390
        idx = idx.sibling(idx.row(), 0)
391
        it = self.model.itemFromIndex(idx)
392
        if not it:
393
            return None
394
        node = it.data()
395
        if not node:
396
            print("No node for item:", it, it.text())
397
            return None
398
        return node
399
400
401
class Window(QMainWindow):
402
403
    def __init__(self):
404
        QMainWindow.__init__(self)
405
        self.ui = Ui_MainWindow()
406
        self.ui.setupUi(self)
407
        self.setWindowIcon(QIcon(":/network.svg"))
408
409
        # fix stuff imposible to do in qtdesigner
410
        # remove dock titlebar for addressbar
411
        w = QWidget()
412
        self.ui.addrDockWidget.setTitleBarWidget(w)
413
        # tabify some docks
414
        self.tabifyDockWidget(self.ui.evDockWidget, self.ui.subDockWidget)
415
        self.tabifyDockWidget(self.ui.subDockWidget, self.ui.refDockWidget)
416
417
        # we only show statusbar in case of errors
418
        self.ui.statusBar.hide()
419
420
        # load settings, seconds arg is default
421
        self.settings = QSettings("FreeOpcUa", "FreeOpcUaClient")
422
        self._address_list = self.settings.value("address_list", ["opc.tcp://localhost:4840", "opc.tcp://localhost:53530/OPCUA/SimulationServer/"])
423
        self._address_list_max_count = int(self.settings.value("address_list_max_count", 10))
424
425
        # init widgets
426
        for addr in self._address_list:
427
            self.ui.addrComboBox.insertItem(-1, addr)
428
429
        self.uaclient = UaClient()
430
431
        self.tree_ui = TreeUI(self, self.uaclient)
432
        self.refs_ui = RefsUI(self, self.uaclient)
433
        self.attrs_ui = AttrsUI(self, self.uaclient)
434
        self.datachange_ui = DataChangeUI(self, self.uaclient)
435
        self.event_ui = EventUI(self, self.uaclient)
436
437
        self.resize(int(self.settings.value("main_window_width", 800)), int(self.settings.value("main_window_height", 600)))
438
        self.restoreState(self.settings.value("main_window_state", b"", type="QByteArray"))
439
440
        self.ui.connectButton.clicked.connect(self._connect)
441
        self.ui.disconnectButton.clicked.connect(self._disconnect)
442
        # self.ui.treeView.expanded.connect(self._fit)
443
444
        self.ui.actionConnect.triggered.connect(self._connect)
445
        self.ui.actionDisconnect.triggered.connect(self._disconnect)
446
447
    def show_error(self, msg, level=1):
448
        print("showing error: ", msg, level)
449
        self.ui.statusBar.show()
450
        self.ui.statusBar.setStyleSheet("QStatusBar { background-color : red; color : black; }")
451
        self.ui.statusBar.showMessage(str(msg))
452
        QTimer.singleShot(1500, self.ui.statusBar.hide)
453
454
    def get_current_node(self, idx=None):
455
        return self.tree_ui.get_current_node(idx)
456
457
    def get_uaclient(self):
458
        return self.uaclient
459
460
    def _connect(self):
461
        uri = self.ui.addrComboBox.currentText()
462
        try:
463
            self.uaclient.connect(uri)
464
        except Exception as ex:
465
            self.show_error(ex)
466
            raise
467
468
        self._update_address_list(uri)
469
        self.tree_ui.start()
470
471
    def _update_address_list(self, uri):
472
        if uri == self._address_list[0]:
473
            return
474
        if uri in self._address_list:
475
            self._address_list.remove(uri)
476
        self._address_list.insert(0, uri)
477
        if len(self._address_list) > self._address_list_max_count:
478
            self._address_list.pop(-1)
479
480
    def _disconnect(self):
481
        try:
482
            self.uaclient.disconnect()
483
        except Exception as ex:
484
            self.show_error(ex)
485
            raise
486
        finally:
487
            self.tree_ui.clear()
488
            self.refs_ui.clear()
489
            self.attrs_ui.clear()
490
            self.datachange_ui.clear()
491
            self.event_ui.clear()
492
493
    def closeEvent(self, event):
494
        self.settings.setValue("main_window_width", self.size().width())
495
        self.settings.setValue("main_window_height", self.size().height())
496
        self.settings.setValue("main_window_state", self.saveState())
497
        self.settings.setValue("address_list", self._address_list)
498
        self._disconnect()
499
        event.accept()
500
501
502
class TreeViewModel(QStandardItemModel):
503
504
    error = pyqtSignal(str)
505
506
    def __init__(self, uaclient):
507
        super(TreeViewModel, self).__init__()
508
        self.uaclient = uaclient
509
        self._fetched = []
510
511
    def clear(self):
512
        QStandardItemModel.clear(self)
513
        self._fetched = []
514
        self.setHorizontalHeaderLabels(['DisplayName', "BrowseName", 'NodeId'])
515
516
    def add_item(self, node, desc, parent=None):
517
        item = [QStandardItem(desc.DisplayName.to_string()), QStandardItem(desc.BrowseName.to_string()), QStandardItem(desc.NodeId.to_string())]
518
        if desc.NodeClass == ua.NodeClass.Object:
519
            if desc.TypeDefinition == ua.TwoByteNodeId(ua.ObjectIds.FolderType):
520
                item[0].setIcon(QIcon(":/folder.svg"))
521
            else:
522
                item[0].setIcon(QIcon(":/object.svg"))
523
        elif desc.NodeClass == ua.NodeClass.Variable:
524
            if desc.TypeDefinition == ua.TwoByteNodeId(ua.ObjectIds.PropertyType):
525
                item[0].setIcon(QIcon(":/property.svg"))
526
            else:
527
                item[0].setIcon(QIcon(":/variable.svg"))
528
        elif desc.NodeClass == ua.NodeClass.Method:
529
                item[0].setIcon(QIcon(":/method.svg"))
530
531
        item[0].setData(node)
532
        if parent:
533
            return parent.appendRow(item)
534
        else:
535
            return self.appendRow(item)
536
537
    def canFetchMore(self, idx):
538
        item = self.itemFromIndex(idx)
539
        if not item:
540
            return True
541
        node = item.data()
542
        if node not in self._fetched:
543
            self._fetched.append(node)
544
            return True
545
        return False
546
547
    def hasChildren(self, idx):
548
        item = self.itemFromIndex(idx)
549
        if not item:
550
            return True
551
        node = item.data()
552
        if node in self._fetched:
553
            return QStandardItemModel.hasChildren(self, idx)
554
        return True
555
556
    def fetchMore(self, idx):
557
        parent = self.itemFromIndex(idx)
558
        if parent:
559
            self._fetchMore(parent)
560
561
    def _fetchMore(self, parent):
562
        try:
563
            for node, attrs in self.uaclient.get_children(parent.data()).items():
564
                self.add_item(node, attrs, parent)
565
        except Exception as ex:
566
            self.error.emit(ex)
567
            raise
568
569
    #def flags(self, idx):
570
        #item = self.itemFromIndex(idx)
571
        #flags = QStandardItemModel.flags(self, idx)
572
        #if not item:
573
            #return flags
574
        #node = item.data()
575
        #if node and node.get_node_class() == ua.NodeClass.Variable:
576
            ## FIXME not efficient to query, should be stored in data()
577
            ##print(1, flags)
578
            #return flags | Qt.ItemIsDropEnabled
579
        #else:
580
            #print(2, flags)
581
            #return flags
582
583
    #def mimeTypes(self):
584
        #return ["application/vnd.text.list"]
585
586
    def mimeData(self, idxs):
587
        mdata = QMimeData()
588
        nodes = []
589
        for idx in idxs:
590
            item = self.itemFromIndex(idx)
591
            if item:
592
                node = item.data()
593
                if node:
594
                    nodes.append(node.nodeid.to_string())
595
        mdata.setText(", ".join(nodes))
596
        return mdata
597
598
599
def main():
600
    app = QApplication(sys.argv)
601
    client = Window()
602
    client.show()
603
    sys.exit(app.exec_())
604
605
606
if __name__ == "__main__":
607
    main()
608