Completed
Push — master ( e41243...693ddc )
by Olivier
23s
created

Window.setup_context_menu_tree()   A

Complexity

Conditions 1

Size

Total Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
c 0
b 0
f 0
dl 0
loc 9
rs 9.6666
1
#! /usr/bin/env python3
2
3
import sys
4
5
from datetime import datetime
6
import inspect
7
from enum import Enum
8
import logging
9
10
from PyQt5.QtCore import pyqtSignal, QTimer, Qt, QObject, QSettings, QItemSelection, QMimeData, QCoreApplication
11
from PyQt5.QtGui import QStandardItemModel, QStandardItem, QIcon
12
from PyQt5.QtWidgets import QMainWindow, QWidget, QApplication, QAbstractItemView, QMenu, QAction
13
14
from opcua import ua
15
from opcua import Node
16
17
from uaclient.uaclient import UaClient
18
from uaclient.mainwindow_ui import Ui_MainWindow
19
from uaclient.connection_dialog import ConnectionDialog
20
from uaclient.graphwidget import GraphUI
21
22
from uawidgets import resources
23
from uawidgets.attrs_widget import AttrsWidget
24
from uawidgets.tree_widget import TreeWidget
25
from uawidgets.refs_widget import RefsWidget
26
from uawidgets.utils import trycatchslot
27
from uawidgets.logger import QtHandler
28
from uawidgets.call_method_dialog import CallMethodDialog
29
30
31
logger = logging.getLogger(__name__)
32
33
34
class DataChangeHandler(QObject):
35
    data_change_fired = pyqtSignal(object, str, str)
36
37
    def datachange_notification(self, node, val, data):
38
        if data.monitored_item.Value.SourceTimestamp:
39
            dato = data.monitored_item.Value.SourceTimestamp.isoformat()
40
        elif data.monitored_item.Value.ServerTimestamp:
41
            dato = data.monitored_item.Value.ServerTimestamp.isoformat()
42
        else:
43
            dato = datetime.now().isoformat()
44
        self.data_change_fired.emit(node, str(val), dato)
45
46
47
class EventHandler(QObject):
48
    event_fired = pyqtSignal(object)
49
50
    def event_notification(self, event):
51
        self.event_fired.emit(event)
52
53
54
class EventUI(object):
55
56 View Code Duplication
    def __init__(self, window, uaclient):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
57
        self.window = window
58
        self.uaclient = uaclient
59
        self._handler = EventHandler()
60
        self._subscribed_nodes = []  # FIXME: not really needed
61
        self.model = QStandardItemModel()
62
        self.window.ui.evView.setModel(self.model)
63
        self.window.ui.actionSubscribeEvent.triggered.connect(self._subscribe)
64
        self.window.ui.actionUnsubscribeEvents.triggered.connect(self._unsubscribe)
65
        # context menu
66
        self.window.addAction(self.window.ui.actionSubscribeEvent)
67
        self.window.addAction(self.window.ui.actionUnsubscribeEvents)
68
        self._handler.event_fired.connect(self._update_event_model, type=Qt.QueuedConnection)
69
70
        # accept drops
71
        self.model.canDropMimeData = self.canDropMimeData
72
        self.model.dropMimeData = self.dropMimeData
73
74
    def canDropMimeData(self, mdata, action, row, column, parent):
75
        return True
76
77
    def show_error(self, *args):
78
        self.window.show_error(*args)
79
80
    def dropMimeData(self, mdata, action, row, column, parent):
81
        node = self.uaclient.client.get_node(mdata.text())
82
        self._subscribe(node)
83
        return True
84
85
    def clear(self):
86
        self._subscribed_nodes = []
87
        self.model.clear()
88
89
    @trycatchslot
90
    def _subscribe(self, node=None):
91
        logger.info("Subscribing to %s", node)
92
        if not node:
93
            node = self.window.get_current_node()
94
            if node is None:
95
                return
96
        if node in self._subscribed_nodes:
97
            logger.info("already subscribed to event for node: %s", node)
98
            return
99
        logger.info("Subscribing to events for %s", node)
100
        self.window.ui.evDockWidget.raise_()
101
        try:
102
            self.uaclient.subscribe_events(node, self._handler)
103
        except Exception as ex:
104
            self.window.show_error(ex)
105
            raise
106
        else:
107
            self._subscribed_nodes.append(node)
108
109
    @trycatchslot
110
    def _unsubscribe(self):
111
        node = self.window.get_current_node()
112
        if node is None:
113
            return
114
        self._subscribed_nodes.remove(node)
115
        self.uaclient.unsubscribe_events(node)
116
117
    @trycatchslot
118
    def _update_event_model(self, event):
119
        self.model.appendRow([QStandardItem(str(event))])
120
121
122
class DataChangeUI(object):
123
124 View Code Duplication
    def __init__(self, window, uaclient):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
125
        self.window = window
126
        self.uaclient = uaclient
127
        self._subhandler = DataChangeHandler()
128
        self._subscribed_nodes = []
129
        self.model = QStandardItemModel()
130
        self.window.ui.subView.setModel(self.model)
131
        self.window.ui.subView.horizontalHeader().setSectionResizeMode(1)
132
133
        self.window.ui.actionSubscribeDataChange.triggered.connect(self._subscribe)
134
        self.window.ui.actionUnsubscribeDataChange.triggered.connect(self._unsubscribe)
135
136
        # populate contextual menu
137
        self.window.addAction(self.window.ui.actionSubscribeDataChange)
138
        self.window.addAction(self.window.ui.actionUnsubscribeDataChange)
139
140
        # handle subscriptions
141
        self._subhandler.data_change_fired.connect(self._update_subscription_model, type=Qt.QueuedConnection)
142
        
143
        # accept drops
144
        self.model.canDropMimeData = self.canDropMimeData
145
        self.model.dropMimeData = self.dropMimeData
146
147
    def canDropMimeData(self, mdata, action, row, column, parent):
148
        return True
149
150
    def dropMimeData(self, mdata, action, row, column, parent):
151
        node = self.uaclient.client.get_node(mdata.text())
152
        self._subscribe(node)
153
        return True
154
155
    def clear(self):
156
        self._subscribed_nodes = []
157
        self.model.clear()
158
159
    def show_error(self, *args):
160
        self.window.show_error(*args)
161
162
    @trycatchslot
163
    def _subscribe(self, node=None):
164
        if not isinstance(node, Node):
165
            node = self.window.get_current_node()
166
            if node is None:
167
                return
168
        if node in self._subscribed_nodes:
169
            logger.warning("allready subscribed to node: %s ", node)
170
            return
171
        self.model.setHorizontalHeaderLabels(["DisplayName", "Value", "Timestamp"])
172
        text = str(node.get_display_name().Text)
173
        row = [QStandardItem(text), QStandardItem("No Data yet"), QStandardItem("")]
174
        row[0].setData(node)
175
        self.model.appendRow(row)
176
        self._subscribed_nodes.append(node)
177
        self.window.ui.subDockWidget.raise_()
178
        try:
179
            self.uaclient.subscribe_datachange(node, self._subhandler)
180
        except Exception as ex:
181
            self.window.show_error(ex)
182
            idx = self.model.indexFromItem(row[0])
183
            self.model.takeRow(idx.row())
184
            raise
185
186
    @trycatchslot
187
    def _unsubscribe(self):
188
        node = self.window.get_current_node()
189
        if node is None:
190
            return
191
        self.uaclient.unsubscribe_datachange(node)
192
        self._subscribed_nodes.remove(node)
193
        i = 0
194
        while self.model.item(i):
195
            item = self.model.item(i)
196
            if item.data() == node:
197
                self.model.removeRow(i)
198
            i += 1
199
200
    def _update_subscription_model(self, node, value, timestamp):
201
        i = 0
202
        while self.model.item(i):
203
            item = self.model.item(i)
204
            if item.data() == node:
205
                it = self.model.item(i, 1)
206
                it.setText(value)
207
                it_ts = self.model.item(i, 2)
208
                it_ts.setText(timestamp)
209
            i += 1
210
211
212
class Window(QMainWindow):
213
214
    def __init__(self):
215
        QMainWindow.__init__(self)
216
        self.ui = Ui_MainWindow()
217
        self.ui.setupUi(self)
218
        self.setWindowIcon(QIcon(":/network.svg"))
219
220
        # fix stuff imposible to do in qtdesigner
221
        # remove dock titlebar for addressbar
222
        w = QWidget()
223
        self.ui.addrDockWidget.setTitleBarWidget(w)
224
        # tabify some docks
225
        self.tabifyDockWidget(self.ui.evDockWidget, self.ui.subDockWidget)
226
        self.tabifyDockWidget(self.ui.subDockWidget, self.ui.refDockWidget)
227
        self.tabifyDockWidget(self.ui.refDockWidget, self.ui.graphDockWidget)
228
229
        # we only show statusbar in case of errors
230
        self.ui.statusBar.hide()
231
232
        # setup QSettings for application and get a settings object
233
        QCoreApplication.setOrganizationName("FreeOpcUa")
234
        QCoreApplication.setApplicationName("OpcUaClient")
235
        self.settings = QSettings()
236
237
        self._address_list = self.settings.value("address_list", ["opc.tcp://localhost:4840", "opc.tcp://localhost:53530/OPCUA/SimulationServer/"])
238
        self._address_list_max_count = int(self.settings.value("address_list_max_count", 10))
239
240
        # init widgets
241
        for addr in self._address_list:
242
            self.ui.addrComboBox.insertItem(-1, addr)
243
244
        self.uaclient = UaClient()
245
246
        self.tree_ui = TreeWidget(self.ui.treeView)
247
        self.tree_ui.error.connect(self.show_error)
248
        self.setup_context_menu_tree()
249
        self.ui.treeView.selectionModel().currentChanged.connect(self._update_actions_state)
250
251
        self.refs_ui = RefsWidget(self.ui.refView)
252
        self.refs_ui.error.connect(self.show_error)
253
        self.attrs_ui = AttrsWidget(self.ui.attrView)
254
        self.attrs_ui.error.connect(self.show_error)
255
        self.datachange_ui = DataChangeUI(self, self.uaclient)
256
        self.event_ui = EventUI(self, self.uaclient)
257
        self.graph_ui = GraphUI(self, self.uaclient)
258
259
        self.ui.addrComboBox.currentTextChanged.connect(self._uri_changed)
260
        self._uri_changed(self.ui.addrComboBox.currentText())  # force update for current value at startup
261
262
        self.ui.treeView.selectionModel().selectionChanged.connect(self.show_refs)
263
        self.ui.actionCopyPath.triggered.connect(self.tree_ui.copy_path)
264
        self.ui.actionCopyNodeId.triggered.connect(self.tree_ui.copy_nodeid)
265
        self.ui.actionCall.triggered.connect(self.call_method)
266
267
        self.ui.treeView.selectionModel().selectionChanged.connect(self.show_attrs)
268
        self.ui.attrRefreshButton.clicked.connect(self.show_attrs)
269
270
        self.resize(int(self.settings.value("main_window_width", 800)), int(self.settings.value("main_window_height", 600)))
271
        data = self.settings.value("main_window_state", None)
272
        if data:
273
            self.restoreState(data)
274
275
        self.ui.connectButton.clicked.connect(self.connect)
276
        self.ui.disconnectButton.clicked.connect(self.disconnect)
277
        # self.ui.treeView.expanded.connect(self._fit)
278
279
        self.ui.actionConnect.triggered.connect(self.connect)
280
        self.ui.actionDisconnect.triggered.connect(self.disconnect)
281
282
        self.ui.connectOptionButton.clicked.connect(self.show_connection_dialog)
283
284
    def _uri_changed(self, uri):
285
        self.uaclient.load_security_settings(uri)
286
287
    def show_connection_dialog(self):
288
        dia = ConnectionDialog(self, self.ui.addrComboBox.currentText())
289
        dia.security_mode = self.uaclient.security_mode
290
        dia.security_policy = self.uaclient.security_policy
291
        dia.certificate_path = self.uaclient.certificate_path
292
        dia.private_key_path = self.uaclient.private_key_path
293
        ret = dia.exec_()
294
        if ret:
295
            self.uaclient.security_mode = dia.security_mode
296
            self.uaclient.security_policy = dia.security_policy
297
            self.uaclient.certificate_path = dia.certificate_path
298
            self.uaclient.private_key_path = dia.private_key_path
299
300
    @trycatchslot
301
    def show_refs(self, selection):
302
        if isinstance(selection, QItemSelection):
303
            if not selection.indexes(): # no selection
304
                return
305
306
        node = self.get_current_node()
307
        if node:
308
            self.refs_ui.show_refs(node)
309
    
310
    @trycatchslot
311
    def show_attrs(self, selection):
312
        if isinstance(selection, QItemSelection):
313
            if not selection.indexes(): # no selection
314
                return
315
316
        node = self.get_current_node()
317
        if node:
318
            self.attrs_ui.show_attrs(node)
319
320
    def show_error(self, msg):
321
        logger.warning("showing error: %s")
322
        self.ui.statusBar.show()
323
        self.ui.statusBar.setStyleSheet("QStatusBar { background-color : red; color : black; }")
324
        self.ui.statusBar.showMessage(str(msg))
325
        QTimer.singleShot(1500, self.ui.statusBar.hide)
326
327
    def get_current_node(self, idx=None):
328
        return self.tree_ui.get_current_node(idx)
329
330
    def get_uaclient(self):
331
        return self.uaclient
332
333
    @trycatchslot
334
    def connect(self):
335
        uri = self.ui.addrComboBox.currentText()
336
        try:
337
            self.uaclient.connect(uri)
338
        except Exception as ex:
339
            self.show_error(ex)
340
            raise
341
342
        self._update_address_list(uri)
343
        self.tree_ui.set_root_node(self.uaclient.client.get_root_node())
344
        self.ui.treeView.setFocus()
345
        self.load_current_node()
346
347
    def _update_address_list(self, uri):
348
        if uri == self._address_list[0]:
349
            return
350
        if uri in self._address_list:
351
            self._address_list.remove(uri)
352
        self._address_list.insert(0, uri)
353
        if len(self._address_list) > self._address_list_max_count:
354
            self._address_list.pop(-1)
355
356
    def disconnect(self):
357
        try:
358
            self.uaclient.disconnect()
359
        except Exception as ex:
360
            self.show_error(ex)
361
            raise
362
        finally:
363
            self.save_current_node()
364
            self.tree_ui.clear()
365
            self.refs_ui.clear()
366
            self.attrs_ui.clear()
367
            self.datachange_ui.clear()
368
            self.event_ui.clear()
369
370
371
    def closeEvent(self, event):
372
        self.tree_ui.save_state()
373
        self.attrs_ui.save_state()
374
        self.refs_ui.save_state()
375
        self.settings.setValue("main_window_width", self.size().width())
376
        self.settings.setValue("main_window_height", self.size().height())
377
        self.settings.setValue("main_window_state", self.saveState())
378
        self.settings.setValue("address_list", self._address_list)
379
        self.disconnect()
380
        event.accept()
381
382
    def save_current_node(self):
383
        current_node = self.tree_ui.get_current_node()
384
        if current_node:
385
            mysettings = self.settings.value("current_node", None)
386
            if mysettings is None:
387
                mysettings = {}
388
            uri = self.ui.addrComboBox.currentText()
389
            mysettings[uri] = current_node.nodeid.to_string()
390
            self.settings.setValue("current_node", mysettings)
391
392
    def load_current_node(self):
393
        mysettings = self.settings.value("current_node", None)
394
        if mysettings is None:
395
            return
396
        uri = self.ui.addrComboBox.currentText()
397
        if uri in mysettings:
398
            nodeid = ua.NodeId.from_string(mysettings[uri])
399
            node = self.uaclient.client.get_node(nodeid)
400
            self.tree_ui.expand_to_node(node)
401
402
    def setup_context_menu_tree(self):
403
        self.ui.treeView.setContextMenuPolicy(Qt.CustomContextMenu)
404
        self.ui.treeView.customContextMenuRequested.connect(self._show_context_menu_tree)
405
        self._contextMenu = QMenu()
406
        self.addAction(self.ui.actionCopyPath)
407
        self.addAction(self.ui.actionCopyNodeId)
408
        self._contextMenu.addSeparator()
409
        self._contextMenu.addAction(self.ui.actionCall)
410
        self._contextMenu.addSeparator()
411
412
    def addAction(self, action):
413
        self._contextMenu.addAction(action)
414
415
    @trycatchslot
416
    def _update_actions_state(self, current, previous):
417
        node = self.get_current_node(current)
418
        self.ui.actionCall.setEnabled(False)
419
        if node:
420
            if node.get_node_class() == ua.NodeClass.Method:
421
                self.ui.actionCall.setEnabled(True)
422
423
    def _show_context_menu_tree(self, position):
424
        node = self.tree_ui.get_current_node()
425
        if node:
426
            self._contextMenu.exec_(self.ui.treeView.viewport().mapToGlobal(position))
427
428
    def call_method(self):
429
        node = self.get_current_node()
430
        dia = CallMethodDialog(self, self.uaclient.client, node)
431
        dia.show()
432
433
434
def main():
435
    app = QApplication(sys.argv)
436
    client = Window()
437
    handler = QtHandler(client.ui.logTextEdit)
438
    logging.getLogger().addHandler(handler)
439
    logging.getLogger("uaclient").setLevel(logging.INFO)
440
    logging.getLogger("uawidgets").setLevel(logging.INFO)
441
    #logging.getLogger("opcua").setLevel(logging.INFO)  # to enable logging of ua client library
442
   
443
    client.show()
444
    sys.exit(app.exec_())
445
446
447
if __name__ == "__main__":
448
    main()
449