Completed
Push — master ( 8ef529...d1d067 )
by Olivier
01:10
created

Window.show_attrs()   A

Complexity

Conditions 2

Size

Total Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

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