Completed
Push — master ( b806e7...a0ec22 )
by Olivier
53s
created

freeopcuaclient.TreeUI.__init__()   A

Complexity

Conditions 1

Size

Total Lines 15

Duplication

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