Completed
Push — master ( 44c72f...fa0a1c )
by Olivier
56s
created

EventUI.dropMimeData()   A

Complexity

Conditions 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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