From ab7ab78d51ade137ad2b47e7edc0b26099f45801 Mon Sep 17 00:00:00 2001
From: Jan Rach <rachj@students.zcu.cz>
Date: Mon, 24 May 2021 08:31:08 +0000
Subject: [PATCH 01/12] Feature/8923 grid layout

---
 aswi2021vochomurka/view/main_view.py | 96 ++++++++++++++++------------
 1 file changed, 54 insertions(+), 42 deletions(-)

diff --git a/aswi2021vochomurka/view/main_view.py b/aswi2021vochomurka/view/main_view.py
index a769968..0f154a2 100644
--- a/aswi2021vochomurka/view/main_view.py
+++ b/aswi2021vochomurka/view/main_view.py
@@ -1,29 +1,17 @@
 import logging
-import math
-import random
 
+import matplotlib.pyplot as plt
+from PyQt5 import QtCore
 from PyQt5.QtCore import QSize, QThread, QObject, pyqtSignal
-from PyQt5.QtWidgets import QMainWindow, QPlainTextEdit, QDialog, QHBoxLayout
-from numpy import pi, sin, cos, tan, exp
-from matplotlib.pyplot import subplot
+from PyQt5.QtWidgets import QDialog, QPushButton, QVBoxLayout
+from PyQt5.QtWidgets import QHBoxLayout, QGridLayout, QScrollArea, QWidget
+from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
 
 from aswi2021vochomurka.model.Message import Message
 from aswi2021vochomurka.service.mqtt.mqtt_subscriber import MQTTSubscriber
 from aswi2021vochomurka.service.subscriber import Subscriber
 from aswi2021vochomurka.service.subscriber_callback import SubscriberCallback
 from aswi2021vochomurka.service.subscriber_params import SubscriberParams, ConnectionParams
-
-from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
-from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NavigationToolbar
-import matplotlib.pyplot as plt
-
-import sys
-from PyQt5.QtWidgets import QDialog, QApplication, QPushButton, QVBoxLayout
-from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
-from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NavigationToolbar
-import matplotlib.pyplot as plt
-import random
-
 from aswi2021vochomurka.view.logger_view import LoggerView
 
 
@@ -31,11 +19,11 @@ class Worker(QObject, SubscriberCallback):
     connected = pyqtSignal()
     disconnected = pyqtSignal()
     error = pyqtSignal(Exception)
-    newMessage = pyqtSignal(str)
+    newMessage = pyqtSignal(Message)
     subscriber: Subscriber = None
 
     params = SubscriberParams(
-        ["/home/1", "/home/2"],
+        ["/home/1", "/home/2", "/home/3", "/home/4", "/home/5", "/home/6", "/home/7", "/home/8"],
         10,
         ConnectionParams("localhost", 1883, 60),
         True
@@ -55,8 +43,8 @@ class Worker(QObject, SubscriberCallback):
         self.error.emit()
 
     def onMessage(self, message: Message):
-        self.newMessage.emit(message.topic)
-        self.window.plot(message)
+        self.newMessage.emit(message)
+        # self.window.plot(message)
 
     def onCloseTopic(self, topic: str):
         pass
@@ -75,12 +63,13 @@ class MainView(QDialog):
         self.dataIndex = 0
         self.dataDict = {}
 
-        self.figure = plt.figure(figsize=([500,500]))
+        self.canvasDict = {}
+        self.figureDict = {}
 
-        self.canvas = FigureCanvas(self.figure)
-        self.toolbar = NavigationToolbar(self.canvas, self)
+        # self.toolbar = NavigationToolbar(self.canvas, self)
 
-        self.setMinimumSize(QSize(440, 240))
+        # self.setMinimumSize(QSize(440, 240))
+        self.setMinimumSize(QSize(1200, 800))
         self.setWindowTitle("MQTT demo")
 
         # Add logger text field
@@ -92,39 +81,62 @@ class MainView(QDialog):
 
         layout = QVBoxLayout()
         layout.addWidget(logger.widget)
-        layout.addWidget(self.toolbar)
-        layout.addWidget(self.canvas)
+        # layout.addWidget(self.toolbar)
 
         self.setLayout(layout)
 
-        self.initSubscriber()
+        scrollArea = QScrollArea(self)
+        scrollArea.setWidgetResizable(True)
+        scrollContent = QWidget()
+        self.grid = QGridLayout(scrollContent)
+        scrollArea.setWidget(scrollContent)
+        layout.addWidget(scrollArea)
 
-    def plot(self, message: Message):
-        self.figure.clear()
+        self.init_subscriber()
 
+    def plot(self, message: Message):
         if message.topic in self.dataDict:
             self.dataDict[message.topic].append(message.value)
+
+            figure = self.figureDict[message.topic]
+            figure.clear()
+
+            plt.figure(figure.number)
+            plt.plot(self.dataDict[message.topic])
+
+            self.canvasDict[message.topic].draw()
         else:
             self.dataDict[message.topic] = [message.value]
-            self.chartsNum += 1
 
-        rows = math.ceil(self.chartsNum / 2)
+            figure = plt.figure(figsize=[500, 500])
+            canvas = FigureCanvas(figure)
+            layout = QHBoxLayout()
 
-        b = 0
-        for a in self.dataDict.values():
-            self.figure.add_subplot(rows, 2, b + 1)
-            b += 1
-            plt.plot(a)
+            plt.plot(self.dataDict[message.topic])
 
-        self.canvas.draw()
+            self.canvasDict[message.topic] = canvas
+            self.figureDict[message.topic] = figure
+
+            widget = QWidget()
+            widget.setLayout(layout)
+            button = QPushButton(':')
+            button.setFixedSize(QSize(40, 40))
+            layout.addWidget(canvas)
+            layout.addWidget(button)
+            layout.setAlignment(button, QtCore.Qt.AlignTop)
+            widget.setMinimumSize(QSize(500, 500))
+
+            self.grid.addWidget(widget, int(self.chartsNum / 2), self.chartsNum % 2)
+
+            self.chartsNum += 1
 
-    def initSubscriber(self):
+    def init_subscriber(self):
         self.workerThread = QThread()
         self.worker = Worker()
         self.worker.moveToThread(self.workerThread)
         self.workerThread.started.connect(self.worker.start)
-        # self.worker.newMessage.connect(
-        #    lambda message: self.b.insertPlainText(message + "\n")
-        # )
+        self.worker.newMessage.connect(
+            lambda message: self.plot(message)
+        )
         self.worker.window = self
         self.workerThread.start()
-- 
GitLab


From be89639f4c19b9b39247f2990da1ef80d0950da5 Mon Sep 17 00:00:00 2001
From: Martin Forejt <mforejt@students.zcu.cz>
Date: Mon, 24 May 2021 12:46:43 +0000
Subject: [PATCH 02/12] Feature/8921 preferences dialog

---
 .../service/mqtt/mqtt_subscriber.py           |  11 +-
 aswi2021vochomurka/service/subscriber.py      |  11 +-
 aswi2021vochomurka/view/main_view.py          |  96 +++++++++++---
 aswi2021vochomurka/view/settings.py           | 120 ++++++++++++++++++
 4 files changed, 212 insertions(+), 26 deletions(-)
 create mode 100644 aswi2021vochomurka/view/settings.py

diff --git a/aswi2021vochomurka/service/mqtt/mqtt_subscriber.py b/aswi2021vochomurka/service/mqtt/mqtt_subscriber.py
index be7b979..fb07edb 100644
--- a/aswi2021vochomurka/service/mqtt/mqtt_subscriber.py
+++ b/aswi2021vochomurka/service/mqtt/mqtt_subscriber.py
@@ -7,6 +7,7 @@ from aswi2021vochomurka.service.subscriber import Subscriber
 
 
 class MQTTSubscriber(Subscriber):
+    client: mqtt.Client = None
 
     # The callback for when the client receives a CONNACK response from the server.
     def on_connect(self, client, userdata, flags, rc, properties=None):
@@ -40,6 +41,7 @@ class MQTTSubscriber(Subscriber):
     def start(self):
         super().start()
         client = mqtt.Client()
+        self.client = client
         client.on_connect = self.on_connect
         client.on_message = self.on_message
         client.on_disconnect = self.on_disconnect
@@ -47,7 +49,6 @@ class MQTTSubscriber(Subscriber):
 
         if not self.params.anonymous:
             logging.info('Using credentials, username=' + self.params.username + ', password=' + self.params.password)
-            client.tls_set()
             client.username_pw_set(self.params.username, self.params.password)
 
         try:
@@ -63,3 +64,11 @@ class MQTTSubscriber(Subscriber):
             return
 
         client.loop_forever()
+
+    def stop(self):
+        super().stop()
+        if self.client is not None:
+            logging.info("Disconnecting from broker")
+            client = self.client
+            self.client = None
+            client.disconnect()
diff --git a/aswi2021vochomurka/service/subscriber.py b/aswi2021vochomurka/service/subscriber.py
index 53c4fe1..25ce50b 100644
--- a/aswi2021vochomurka/service/subscriber.py
+++ b/aswi2021vochomurka/service/subscriber.py
@@ -1,19 +1,20 @@
 import time
+from typing import Dict
 
 from apscheduler.schedulers.background import BackgroundScheduler
+from apscheduler.schedulers.base import STATE_STOPPED
 
 from aswi2021vochomurka.model.Message import Message
 from aswi2021vochomurka.service.file_manager import FileManager
 from aswi2021vochomurka.service.subscriber_callback import SubscriberCallback
 from aswi2021vochomurka.service.subscriber_params import SubscriberParams
-from typing import Dict
 
 
 class Subscriber:
     callback: SubscriberCallback
     params: SubscriberParams
 
-    scheduler = BackgroundScheduler()
+    scheduler: BackgroundScheduler
     files: Dict[str, FileManager] = {}
 
     def __init__(self, callback: SubscriberCallback, params: SubscriberParams):
@@ -22,17 +23,19 @@ class Subscriber:
 
     def start(self):
         # start scheduler to check closed topics
+        self.scheduler = BackgroundScheduler()
         self.scheduler.add_job(self.check_closed_topics, 'interval', seconds=self.params.closeLimit)
         self.scheduler.start()
 
     def stop(self):
-        self.scheduler.shutdown()
+        if self.scheduler.state != STATE_STOPPED:
+            self.scheduler.shutdown()
         self.close_files()
 
     def close_files(self):
         for topic in self.files:
             self.files.get(topic).close()
-        self.files = {}
+        self.files.clear()
 
     def check_closed_topics(self):
         t = time.time()
diff --git a/aswi2021vochomurka/view/main_view.py b/aswi2021vochomurka/view/main_view.py
index 0f154a2..8e34463 100644
--- a/aswi2021vochomurka/view/main_view.py
+++ b/aswi2021vochomurka/view/main_view.py
@@ -2,9 +2,10 @@ import logging
 
 import matplotlib.pyplot as plt
 from PyQt5 import QtCore
-from PyQt5.QtCore import QSize, QThread, QObject, pyqtSignal
-from PyQt5.QtWidgets import QDialog, QPushButton, QVBoxLayout
-from PyQt5.QtWidgets import QHBoxLayout, QGridLayout, QScrollArea, QWidget
+from PyQt5 import QtGui
+from PyQt5.QtCore import QSize, QThread, QObject, pyqtSignal, QSettings
+from PyQt5.QtWidgets import QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QScrollArea, QGridLayout
+from PyQt5.QtWidgets import QMenuBar, QAction, QPushButton
 from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
 
 from aswi2021vochomurka.model.Message import Message
@@ -13,6 +14,8 @@ from aswi2021vochomurka.service.subscriber import Subscriber
 from aswi2021vochomurka.service.subscriber_callback import SubscriberCallback
 from aswi2021vochomurka.service.subscriber_params import SubscriberParams, ConnectionParams
 from aswi2021vochomurka.view.logger_view import LoggerView
+from aswi2021vochomurka.view.settings import SettingsDialog, DEFAULT_HOST, DEFAULT_PORT, DEFAULT_KEEPALIVE, \
+    DEFAULT_ANONYMOUS, DEFAULT_USERNAME, DEFAULT_TIMEOUT, DEFAULT_TOPICS
 
 
 class Worker(QObject, SubscriberCallback):
@@ -21,18 +24,19 @@ class Worker(QObject, SubscriberCallback):
     error = pyqtSignal(Exception)
     newMessage = pyqtSignal(Message)
     subscriber: Subscriber = None
+    params: SubscriberParams
 
-    params = SubscriberParams(
-        ["/home/1", "/home/2", "/home/3", "/home/4", "/home/5", "/home/6", "/home/7", "/home/8"],
-        10,
-        ConnectionParams("localhost", 1883, 60),
-        True
-    )
+    def __init__(self, params: SubscriberParams) -> None:
+        super().__init__()
+        self.params = params
 
     def start(self):
         self.subscriber = MQTTSubscriber(self, self.params)
         self.subscriber.start()
 
+    def stop(self):
+        self.subscriber.stop()
+
     def onConnected(self):
         self.connected.emit()
 
@@ -40,7 +44,7 @@ class Worker(QObject, SubscriberCallback):
         self.disconnected.emit()
 
     def onError(self):
-        self.error.emit()
+        pass
 
     def onMessage(self, message: Message):
         self.newMessage.emit(message)
@@ -50,7 +54,7 @@ class Worker(QObject, SubscriberCallback):
         pass
 
 
-class MainView(QDialog):
+class MainView(QMainWindow):
     worker: Worker = None
     workerThread: QThread = None
 
@@ -70,20 +74,17 @@ class MainView(QDialog):
 
         # self.setMinimumSize(QSize(440, 240))
         self.setMinimumSize(QSize(1200, 800))
-        self.setWindowTitle("MQTT demo")
-
-        # Add logger text field
-        logger = LoggerView(self)
-        formatter = logging.Formatter('%(asctime)s %(message)s', '%H:%M')
-        logger.setFormatter(formatter)
-        logger.setLevel(logging.INFO)
-        logging.getLogger('').addHandler(logger)
+        self.setWindowTitle("MQTT client")
 
+        logger = self._createLoggerView()
         layout = QVBoxLayout()
         layout.addWidget(logger.widget)
         # layout.addWidget(self.toolbar)
 
-        self.setLayout(layout)
+        widget = QWidget()
+        widget.setLayout(layout)
+        self.setCentralWidget(widget)
+        self._createMenuBar()
 
         scrollArea = QScrollArea(self)
         scrollArea.setWidgetResizable(True)
@@ -94,6 +95,21 @@ class MainView(QDialog):
 
         self.init_subscriber()
 
+    def _createLoggerView(self):
+        logger = LoggerView(self)
+        formatter = logging.Formatter('%(asctime)s %(message)s', '%H:%M')
+        logger.setFormatter(formatter)
+        logger.setLevel(logging.INFO)
+        logging.getLogger('').addHandler(logger)
+        return logger
+
+    def _createMenuBar(self):
+        menuBar = QMenuBar(self)
+        settingsAction = QAction("&Settings", self)
+        settingsAction.triggered.connect(self.settings)
+        menuBar.addAction(settingsAction)
+        self.setMenuBar(menuBar)
+
     def plot(self, message: Message):
         if message.topic in self.dataDict:
             self.dataDict[message.topic].append(message.value)
@@ -130,9 +146,27 @@ class MainView(QDialog):
 
             self.chartsNum += 1
 
+    def closeEvent(self, a0: QtGui.QCloseEvent) -> None:
+        self.worker.stop()
+
+    def settings(self):
+        dialog = SettingsDialog()
+        if dialog.exec_():
+            self.reconnect()
+
+    def disconnect(self):
+        self.worker.stop()
+        self.workerThread.quit()
+        self.workerThread.wait()
+
+    def reconnect(self):
+        self.disconnect()
+        self.worker.params = self.getConfigParams()
+        self.workerThread.start()
+
     def init_subscriber(self):
         self.workerThread = QThread()
-        self.worker = Worker()
+        self.worker = Worker(self.getConfigParams())
         self.worker.moveToThread(self.workerThread)
         self.workerThread.started.connect(self.worker.start)
         self.worker.newMessage.connect(
@@ -140,3 +174,23 @@ class MainView(QDialog):
         )
         self.worker.window = self
         self.workerThread.start()
+
+    def getConfigParams(self) -> SubscriberParams:
+        settings = QSettings("Vochomurka", "MQTTClient")
+
+        connection = ConnectionParams(
+            settings.value("connection_host", DEFAULT_HOST, str),
+            settings.value("connection_port", DEFAULT_PORT, int),
+            settings.value("connection_keepalive", DEFAULT_KEEPALIVE, int)
+        )
+
+        params = SubscriberParams(
+            settings.value("topics_items", DEFAULT_TOPICS),
+            settings.value("topics_timeout", DEFAULT_TIMEOUT),
+            connection,
+            settings.value("connection_anonymous", DEFAULT_ANONYMOUS, bool),
+            settings.value("connection_username", DEFAULT_USERNAME, str),
+            settings.value("connection_password", DEFAULT_USERNAME, str),
+        )
+
+        return params
diff --git a/aswi2021vochomurka/view/settings.py b/aswi2021vochomurka/view/settings.py
new file mode 100644
index 0000000..379d615
--- /dev/null
+++ b/aswi2021vochomurka/view/settings.py
@@ -0,0 +1,120 @@
+from PyQt5 import QtCore
+from PyQt5.QtCore import QSettings, QSize
+from PyQt5.QtWidgets import QDialog, QVBoxLayout, QDialogButtonBox, QGroupBox, QFormLayout, QLabel, QLineEdit, QSpinBox, \
+    QCheckBox, QPushButton, QListWidget, QListWidgetItem
+
+DEFAULT_HOST = "localhost"
+DEFAULT_PORT = 1883
+DEFAULT_KEEPALIVE = 60
+DEFAULT_ANONYMOUS = True
+DEFAULT_USERNAME = ""
+DEFAULT_PASSWORD = ""
+DEFAULT_TOPICS = ["/home/1", "/home/2"]
+DEFAULT_TIMEOUT = 60
+
+
+class SettingsDialog(QDialog):
+    topics = DEFAULT_TOPICS
+
+    def __init__(self):
+        super(SettingsDialog, self).__init__(None,
+                                             QtCore.Qt.WindowCloseButtonHint | QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowTitleHint)
+        self.settings = QSettings("Vochomurka", "MQTTClient")
+        self.setWindowTitle("Settings")
+        self.setMinimumSize(QSize(600, 500))
+
+        connectionGroupBox = QGroupBox("Connection")
+        connectionLayout = QFormLayout()
+        self.hostInput = QLineEdit(self.settings.value("connection_host", DEFAULT_HOST, str))
+        connectionLayout.addRow(QLabel("Host:"), self.hostInput)
+        self.portInput = QSpinBox()
+        self.portInput.setMaximum(65535)
+        self.portInput.setValue(self.settings.value("connection_port", DEFAULT_PORT, int))
+        connectionLayout.addRow(QLabel("Port:"), self.portInput)
+        self.keepaliveInput = QSpinBox()
+        self.keepaliveInput.setMaximum(1000)
+        self.keepaliveInput.setValue(self.settings.value("connection_keepalive", DEFAULT_KEEPALIVE, int))
+        connectionLayout.addRow(QLabel("Keepalive(s):"), self.keepaliveInput)
+        self.anonymousInput = QCheckBox()
+        self.anonymousInput.setChecked(self.settings.value("connection_anonymous", DEFAULT_ANONYMOUS, bool))
+        self.anonymousInput.stateChanged.connect(self.anonymousChanged)
+        connectionLayout.addRow(QLabel("Anonymous:"), self.anonymousInput)
+        self.usernameInput = QLineEdit(self.settings.value("connection_username", DEFAULT_USERNAME, str))
+        connectionLayout.addRow(QLabel("Username:"), self.usernameInput)
+        self.passwordInput = QLineEdit(self.settings.value("connection_password", DEFAULT_PASSWORD, str))
+        connectionLayout.addRow(QLabel("Password:"), self.passwordInput)
+        self.anonymousChanged()
+        connectionGroupBox.setLayout(connectionLayout)
+
+        topicsGroupBox = QGroupBox("Topics")
+        topicsLayout = QFormLayout()
+
+        self.topics = self.settings.value("topics_items", DEFAULT_TOPICS, list)
+        self.topicsListWidget = QListWidget()
+        for topic in self.topics:
+            item = QListWidgetItem()
+            item.setText(topic)
+            item.setFlags(item.flags() | QtCore.Qt.ItemIsEditable)
+            self.topicsListWidget.addItem(item)
+
+        topicsLayout.addRow(self.topicsListWidget)
+        add = QPushButton("Add")
+        add.setFixedWidth(60)
+        add.clicked.connect(self.addTopic)
+        remove = QPushButton("Remove")
+        remove.setFixedWidth(60)
+        remove.clicked.connect(self.removeTopic)
+        topicsLayout.addRow(add, remove)
+
+        self.timeoutInput = QSpinBox()
+        self.timeoutInput.setMaximum(1000)
+        self.timeoutInput.setToolTip("Unsubscribe topic and close file when there is not new message after this "
+                                     "timeout (in seconds) expires")
+        timeoutLabel = QLabel("Topic timeout(s):")
+        timeoutLabel.setToolTip("Unsubscribe topic and close file when there is not new message after this "
+                                "timeout (in seconds) expires")
+        self.timeoutInput.setValue(self.settings.value("topics_timeout", DEFAULT_TIMEOUT, int))
+        topicsLayout.addRow(timeoutLabel, self.timeoutInput)
+
+        topicsGroupBox.setLayout(topicsLayout)
+
+        buttonBox = QDialogButtonBox()
+        buttonBox.addButton("Save and Reconnect", QDialogButtonBox.AcceptRole)
+        buttonBox.addButton("Cancel", QDialogButtonBox.RejectRole)
+        buttonBox.accepted.connect(self.accept)
+        buttonBox.rejected.connect(self.reject)
+
+        mainLayout = QVBoxLayout()
+        mainLayout.addWidget(connectionGroupBox)
+        mainLayout.addWidget(topicsGroupBox)
+        mainLayout.addWidget(buttonBox)
+        self.setLayout(mainLayout)
+
+    def addTopic(self):
+        item = QListWidgetItem()
+        item.setText("/topic")
+        item.setFlags(item.flags() | QtCore.Qt.ItemIsEditable)
+        self.topicsListWidget.addItem(item)
+
+    def removeTopic(self):
+        for item in self.topicsListWidget.selectedItems():
+            self.topicsListWidget.takeItem(self.topicsListWidget.row(item))
+
+    def anonymousChanged(self):
+        self.usernameInput.setEnabled(not self.anonymousInput.isChecked())
+        self.passwordInput.setEnabled(not self.anonymousInput.isChecked())
+
+    def accept(self) -> None:
+        super().accept()
+        self.topics = []
+        for index in range(self.topicsListWidget.count()):
+            self.topics.append(self.topicsListWidget.item(index).text())
+
+        self.settings.setValue("topics_items", self.topics)
+        self.settings.setValue("topics_timeout", self.timeoutInput.value())
+        self.settings.setValue("connection_host", self.hostInput.text())
+        self.settings.setValue("connection_port", self.portInput.value())
+        self.settings.setValue("connection_keepalive", self.keepaliveInput.value())
+        self.settings.setValue("connection_anonymous", self.anonymousInput.isChecked())
+        self.settings.setValue("connection_username", self.usernameInput.text())
+        self.settings.setValue("connection_password", self.passwordInput.text())
-- 
GitLab


From f727f55f45ef74a412accabac04cdc7f9715d5d4 Mon Sep 17 00:00:00 2001
From: Martin Forejt <mforejt@students.zcu.cz>
Date: Thu, 27 May 2021 21:56:15 +0000
Subject: [PATCH 03/12] Feature/8996 config file location

---
 aswi2021vochomurka/app.py            | 11 +++++++++++
 aswi2021vochomurka/settings.ini      |  9 +++++++++
 aswi2021vochomurka/view/main_view.py |  4 ++--
 aswi2021vochomurka/view/settings.py  |  7 ++++++-
 4 files changed, 28 insertions(+), 3 deletions(-)
 create mode 100644 aswi2021vochomurka/settings.ini

diff --git a/aswi2021vochomurka/app.py b/aswi2021vochomurka/app.py
index ea63143..a7ec8a6 100644
--- a/aswi2021vochomurka/app.py
+++ b/aswi2021vochomurka/app.py
@@ -1,5 +1,6 @@
 import logging
 
+from PyQt5.QtCore import QSettings, QCoreApplication
 from PyQt5.QtWidgets import QApplication
 
 from aswi2021vochomurka.view.main_view import MainView
@@ -31,3 +32,13 @@ def init_logger():
 
     logging.getLogger('apscheduler').setLevel(logging.WARNING)
     logging.getLogger('matplotlib').setLevel(logging.WARNING)
+
+
+def init_settings():
+    QCoreApplication.setOrganizationName('Vochomurka')
+    QCoreApplication.setOrganizationDomain('vochomurka.org')
+    QCoreApplication.setApplicationName('MQTTClient')
+
+    QSettings.setDefaultFormat(QSettings.IniFormat)
+    QSettings.setPath(QSettings.IniFormat, QSettings.SystemScope, '.')
+    settings = QSettings()
diff --git a/aswi2021vochomurka/settings.ini b/aswi2021vochomurka/settings.ini
new file mode 100644
index 0000000..df13f88
--- /dev/null
+++ b/aswi2021vochomurka/settings.ini
@@ -0,0 +1,9 @@
+[General]
+topics_items=/home/1, /home/2
+topics_timeout=60
+connection_host=localhost
+connection_port=1883
+connection_keepalive=60
+connection_anonymous=true
+connection_username=
+connection_password=
diff --git a/aswi2021vochomurka/view/main_view.py b/aswi2021vochomurka/view/main_view.py
index 8e34463..299b5eb 100644
--- a/aswi2021vochomurka/view/main_view.py
+++ b/aswi2021vochomurka/view/main_view.py
@@ -15,7 +15,7 @@ from aswi2021vochomurka.service.subscriber_callback import SubscriberCallback
 from aswi2021vochomurka.service.subscriber_params import SubscriberParams, ConnectionParams
 from aswi2021vochomurka.view.logger_view import LoggerView
 from aswi2021vochomurka.view.settings import SettingsDialog, DEFAULT_HOST, DEFAULT_PORT, DEFAULT_KEEPALIVE, \
-    DEFAULT_ANONYMOUS, DEFAULT_USERNAME, DEFAULT_TIMEOUT, DEFAULT_TOPICS
+    DEFAULT_ANONYMOUS, DEFAULT_USERNAME, DEFAULT_TIMEOUT, DEFAULT_TOPICS, get_settings
 
 
 class Worker(QObject, SubscriberCallback):
@@ -176,7 +176,7 @@ class MainView(QMainWindow):
         self.workerThread.start()
 
     def getConfigParams(self) -> SubscriberParams:
-        settings = QSettings("Vochomurka", "MQTTClient")
+        settings = get_settings()
 
         connection = ConnectionParams(
             settings.value("connection_host", DEFAULT_HOST, str),
diff --git a/aswi2021vochomurka/view/settings.py b/aswi2021vochomurka/view/settings.py
index 379d615..f76d693 100644
--- a/aswi2021vochomurka/view/settings.py
+++ b/aswi2021vochomurka/view/settings.py
@@ -13,13 +13,18 @@ DEFAULT_TOPICS = ["/home/1", "/home/2"]
 DEFAULT_TIMEOUT = 60
 
 
+def get_settings():
+    settings = QSettings('settings.ini', QSettings.IniFormat)
+    return settings
+
+
 class SettingsDialog(QDialog):
     topics = DEFAULT_TOPICS
 
     def __init__(self):
         super(SettingsDialog, self).__init__(None,
                                              QtCore.Qt.WindowCloseButtonHint | QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowTitleHint)
-        self.settings = QSettings("Vochomurka", "MQTTClient")
+        self.settings = get_settings()
         self.setWindowTitle("Settings")
         self.setMinimumSize(QSize(600, 500))
 
-- 
GitLab


From 523382c19daeef1d2eeec6e3ec868b24b011d36f Mon Sep 17 00:00:00 2001
From: MFori <forejt.martin97@gmail.com>
Date: Fri, 28 May 2021 00:21:07 +0200
Subject: [PATCH 04/12] Re: #8997 - refactoring, comments

---
 aswi2021vochomurka/model/Message.py           |  3 ++
 aswi2021vochomurka/service/file_manager.py    | 24 +++++++++++++++-
 aswi2021vochomurka/service/message_parser.py  |  9 ++++++
 .../service/mqtt/mqtt_subscriber.py           | 23 +++++++++++++--
 aswi2021vochomurka/service/subscriber.py      | 28 +++++++++++++++++++
 .../service/subscriber_params.py              |  6 ++++
 6 files changed, 90 insertions(+), 3 deletions(-)

diff --git a/aswi2021vochomurka/model/Message.py b/aswi2021vochomurka/model/Message.py
index 3e70281..9da2d52 100644
--- a/aswi2021vochomurka/model/Message.py
+++ b/aswi2021vochomurka/model/Message.py
@@ -2,6 +2,9 @@ from recordclass import RecordClass
 
 
 class Message(RecordClass):
+    """
+    Message wrapper
+    """
     topic: str
     index: int
     date: str
diff --git a/aswi2021vochomurka/service/file_manager.py b/aswi2021vochomurka/service/file_manager.py
index 7f29c4a..b9e2b29 100644
--- a/aswi2021vochomurka/service/file_manager.py
+++ b/aswi2021vochomurka/service/file_manager.py
@@ -15,17 +15,32 @@ trans = str.maketrans({
     ".": "_"})
 
 
-def create_filename(message: Message):
+def create_filename(message: Message) -> str:
+    """
+    Create file name based on message data
+    :param message: message
+    :return: filename
+    """
     name = "data/" + message.topic.translate(trans) + "/" + message.date + "_" + message.time + ".csv"
     return name
 
 
 class FileManager:
+    """
+    Helper class for writing incoming message to files
+    Each topic has created own instance of this class
+    """
     topic: str
     lastUpdate: float
     file: TextIO
 
     def __init__(self, topic: str, message: Message):
+        """
+        Constructing new FileManager will create new file and write first message
+        :param topic: topic
+        :param message: message
+        :except when creating new file fails
+        """
         self.topic = topic
         logging.debug('opening file ' + self.topic)
 
@@ -39,10 +54,17 @@ class FileManager:
         self.write(message)
 
     def write(self, message: Message):
+        """
+        Append message to file
+        :param message: message
+        """
         self.file.write(message.date + ";" + message.time + ";" + str(message.index) + ";" + str(message.value) + "\n")
         self.lastUpdate = time.time()
 
     def close(self):
+        """
+        Close file
+        """
         logging.debug('closing file ' + self.topic)
         self.file.flush()
         self.file.close()
diff --git a/aswi2021vochomurka/service/message_parser.py b/aswi2021vochomurka/service/message_parser.py
index 7c9a81a..9787dd3 100644
--- a/aswi2021vochomurka/service/message_parser.py
+++ b/aswi2021vochomurka/service/message_parser.py
@@ -6,10 +6,19 @@ from aswi2021vochomurka.model.Message import Message
 
 
 class ParseException(Exception):
+    """
+    May be throw when message has incorrect format
+    """
     pass
 
 
 def parse_mqtt_message(message: MQTTMessage) -> Message:
+    """
+    Parse MQTTMessage to Message
+    :param message: messsage
+    :return: message
+    :except: when message has incorrect format
+    """
     data = message.payload.decode("utf-8")
     parts = data.split(";")
     logging.debug('Parsing message: ' + data + ', parts: ' + str(len(parts)))
diff --git a/aswi2021vochomurka/service/mqtt/mqtt_subscriber.py b/aswi2021vochomurka/service/mqtt/mqtt_subscriber.py
index fb07edb..3466137 100644
--- a/aswi2021vochomurka/service/mqtt/mqtt_subscriber.py
+++ b/aswi2021vochomurka/service/mqtt/mqtt_subscriber.py
@@ -7,10 +7,16 @@ from aswi2021vochomurka.service.subscriber import Subscriber
 
 
 class MQTTSubscriber(Subscriber):
+    """
+    MQTT subscriber, implementation of Subscriber over MQTT protocol
+    """
     client: mqtt.Client = None
 
-    # The callback for when the client receives a CONNACK response from the server.
     def on_connect(self, client, userdata, flags, rc, properties=None):
+        """
+        The callback for when the client receives a CONNACK response from the server.
+        See: mqtt.Client.on_connect for info about params
+        """
         logging.info('Connected with result code ' + str(rc))
         self.callback.onConnected()
 
@@ -20,8 +26,11 @@ class MQTTSubscriber(Subscriber):
             logging.info('Subscribed to topic: ' + topic)
             client.subscribe(topic)
 
-    # The callback for when a PUBLISH message is received from the server.
     def on_message(self, client, userdata, message: mqtt.MQTTMessage):
+        """
+        The callback for when a PUBLISH message is received from the server.
+        See: mqtt.Client.on_message for info about params
+        """
         try:
             m = parse_mqtt_message(message)
             logging.info('Message: ' + str(m))
@@ -34,11 +43,18 @@ class MQTTSubscriber(Subscriber):
             pass
 
     def on_disconnect(self, client, userdata, rc):
+        """
+        The callback for when the client disconnects from the server.
+        See: mqtt.Client.on_disconnect for info about params
+        """
         logging.info('Disconnected')
         self.callback.onDisconnected()
         self.stop()
 
     def start(self):
+        """
+        Start mqtt client
+        """
         super().start()
         client = mqtt.Client()
         self.client = client
@@ -66,6 +82,9 @@ class MQTTSubscriber(Subscriber):
         client.loop_forever()
 
     def stop(self):
+        """
+        Stop mqtt client
+        """
         super().stop()
         if self.client is not None:
             logging.info("Disconnecting from broker")
diff --git a/aswi2021vochomurka/service/subscriber.py b/aswi2021vochomurka/service/subscriber.py
index 25ce50b..82ad578 100644
--- a/aswi2021vochomurka/service/subscriber.py
+++ b/aswi2021vochomurka/service/subscriber.py
@@ -11,6 +11,11 @@ from aswi2021vochomurka.service.subscriber_params import SubscriberParams
 
 
 class Subscriber:
+    """
+    Subscriber is responsible for establishing communication with broker and notifying
+    about new message via callback
+    Subscriber must be started via 'start' method and stopped via 'stop' method
+    """
     callback: SubscriberCallback
     params: SubscriberParams
 
@@ -18,26 +23,43 @@ class Subscriber:
     files: Dict[str, FileManager] = {}
 
     def __init__(self, callback: SubscriberCallback, params: SubscriberParams):
+        """
+        Constructor
+        :param callback: callback
+        :param params: params
+        """
         self.callback = callback
         self.params = params
 
     def start(self):
+        """
+        Start subscriber
+        """
         # start scheduler to check closed topics
         self.scheduler = BackgroundScheduler()
         self.scheduler.add_job(self.check_closed_topics, 'interval', seconds=self.params.closeLimit)
         self.scheduler.start()
 
     def stop(self):
+        """
+        Stop subscriber
+        """
         if self.scheduler.state != STATE_STOPPED:
             self.scheduler.shutdown()
         self.close_files()
 
     def close_files(self):
+        """
+        Close all open files
+        """
         for topic in self.files:
             self.files.get(topic).close()
         self.files.clear()
 
     def check_closed_topics(self):
+        """
+        May be called periodically for checking for expired timeout for closing topic
+        """
         t = time.time()
         for topic in list(self.files):
             file = self.files.get(topic)
@@ -47,8 +69,14 @@ class Subscriber:
                 self.files.pop(topic)
 
     def write_to_file(self, message: Message):
+        """
+        Write message to file
+        :param message: message
+        """
         if message.topic in self.files:
+            # file exist, just append message
             self.files.get(message.topic).write(message)
         else:
+            # new message for this topic, create new file
             fm = FileManager(message.topic, message)
             self.files[message.topic] = fm
diff --git a/aswi2021vochomurka/service/subscriber_params.py b/aswi2021vochomurka/service/subscriber_params.py
index 7aaa227..e9af434 100644
--- a/aswi2021vochomurka/service/subscriber_params.py
+++ b/aswi2021vochomurka/service/subscriber_params.py
@@ -3,12 +3,18 @@ from typing import List
 
 
 class ConnectionParams(RecordClass):
+    """
+    Connection params to connect to broker
+    """
     host: str
     port: int
     timeout: int
 
 
 class SubscriberParams(RecordClass):
+    """
+    Params for Subscriber
+    """
     # list of topics to subscribe
     topics: List[str]
     # close limit in seconds
-- 
GitLab


From 7d3e8585324d5c530b84069844f10c8157350781 Mon Sep 17 00:00:00 2001
From: MFori <forejt.martin97@gmail.com>
Date: Fri, 28 May 2021 00:40:14 +0200
Subject: [PATCH 05/12] fix: load timeout from ini file as int

---
 aswi2021vochomurka/app.py            | 6 +-----
 aswi2021vochomurka/view/main_view.py | 2 +-
 2 files changed, 2 insertions(+), 6 deletions(-)

diff --git a/aswi2021vochomurka/app.py b/aswi2021vochomurka/app.py
index a7ec8a6..6068b0c 100644
--- a/aswi2021vochomurka/app.py
+++ b/aswi2021vochomurka/app.py
@@ -9,6 +9,7 @@ from aswi2021vochomurka.view.main_view import MainView
 class Application(QApplication):
     def __init__(self, sys_argv):
         init_logger()
+        init_settings()
         super(Application, self).__init__(sys_argv)
         logging.info('App started')
         self.main_view = MainView()
@@ -35,10 +36,5 @@ def init_logger():
 
 
 def init_settings():
-    QCoreApplication.setOrganizationName('Vochomurka')
-    QCoreApplication.setOrganizationDomain('vochomurka.org')
-    QCoreApplication.setApplicationName('MQTTClient')
-
     QSettings.setDefaultFormat(QSettings.IniFormat)
     QSettings.setPath(QSettings.IniFormat, QSettings.SystemScope, '.')
-    settings = QSettings()
diff --git a/aswi2021vochomurka/view/main_view.py b/aswi2021vochomurka/view/main_view.py
index 299b5eb..173ff2a 100644
--- a/aswi2021vochomurka/view/main_view.py
+++ b/aswi2021vochomurka/view/main_view.py
@@ -186,7 +186,7 @@ class MainView(QMainWindow):
 
         params = SubscriberParams(
             settings.value("topics_items", DEFAULT_TOPICS),
-            settings.value("topics_timeout", DEFAULT_TIMEOUT),
+            settings.value("topics_timeout", DEFAULT_TIMEOUT, int),
             connection,
             settings.value("connection_anonymous", DEFAULT_ANONYMOUS, bool),
             settings.value("connection_username", DEFAULT_USERNAME, str),
-- 
GitLab


From c9bff745ffd43995c77a48e3032d0ea19ef241cb Mon Sep 17 00:00:00 2001
From: Jan Rach <rachj@students.zcu.cz>
Date: Fri, 28 May 2021 11:15:06 +0000
Subject: [PATCH 06/12] Feature/9017 graph title

---
 aswi2021vochomurka/view/main_view.py | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/aswi2021vochomurka/view/main_view.py b/aswi2021vochomurka/view/main_view.py
index 173ff2a..6792e70 100644
--- a/aswi2021vochomurka/view/main_view.py
+++ b/aswi2021vochomurka/view/main_view.py
@@ -117,7 +117,8 @@ class MainView(QMainWindow):
             figure = self.figureDict[message.topic]
             figure.clear()
 
-            plt.figure(figure.number)
+            figure = plt.figure(figure.number)
+            figure.suptitle(message.topic)
             plt.plot(self.dataDict[message.topic])
 
             self.canvasDict[message.topic].draw()
@@ -129,6 +130,7 @@ class MainView(QMainWindow):
             layout = QHBoxLayout()
 
             plt.plot(self.dataDict[message.topic])
+            figure.suptitle(message.topic)
 
             self.canvasDict[message.topic] = canvas
             self.figureDict[message.topic] = figure
-- 
GitLab


From ff2d1b99de302ce51512a8a3a3f5a8154bc48ee1 Mon Sep 17 00:00:00 2001
From: Jan Rach <rachj@students.zcu.cz>
Date: Wed, 2 Jun 2021 14:46:54 +0200
Subject: [PATCH 07/12] Re: #9016 - Topic close plot reaction implemented

---
 aswi2021vochomurka/view/main_view.py | 45 +++++++++++++++++++++++-----
 1 file changed, 37 insertions(+), 8 deletions(-)

diff --git a/aswi2021vochomurka/view/main_view.py b/aswi2021vochomurka/view/main_view.py
index 8e34463..76e9388 100644
--- a/aswi2021vochomurka/view/main_view.py
+++ b/aswi2021vochomurka/view/main_view.py
@@ -23,6 +23,7 @@ class Worker(QObject, SubscriberCallback):
     disconnected = pyqtSignal()
     error = pyqtSignal(Exception)
     newMessage = pyqtSignal(Message)
+    closeTopic = pyqtSignal(str)
     subscriber: Subscriber = None
     params: SubscriberParams
 
@@ -48,10 +49,11 @@ class Worker(QObject, SubscriberCallback):
 
     def onMessage(self, message: Message):
         self.newMessage.emit(message)
-        # self.window.plot(message)
 
     def onCloseTopic(self, topic: str):
-        pass
+        print("Close topic")
+        self.closeTopic.emit(topic)
+        #pass
 
 
 class MainView(QMainWindow):
@@ -66,9 +68,11 @@ class MainView(QMainWindow):
 
         self.dataIndex = 0
         self.dataDict = {}
-
         self.canvasDict = {}
         self.figureDict = {}
+        self.widgetDict = {}
+
+        self.widgetList = []
 
         # self.toolbar = NavigationToolbar(self.canvas, self)
 
@@ -126,7 +130,7 @@ class MainView(QMainWindow):
 
             figure = plt.figure(figsize=[500, 500])
             canvas = FigureCanvas(figure)
-            layout = QHBoxLayout()
+            self.layout = QHBoxLayout()
 
             plt.plot(self.dataDict[message.topic])
 
@@ -134,18 +138,40 @@ class MainView(QMainWindow):
             self.figureDict[message.topic] = figure
 
             widget = QWidget()
-            widget.setLayout(layout)
+            self.widgetDict[message.topic] = widget
+            self.widgetList.append(widget)
+            widget.setLayout(self.layout)
             button = QPushButton(':')
             button.setFixedSize(QSize(40, 40))
-            layout.addWidget(canvas)
-            layout.addWidget(button)
-            layout.setAlignment(button, QtCore.Qt.AlignTop)
+            self.layout.addWidget(canvas)
+            self.layout.addWidget(button)
+            self.layout.setAlignment(button, QtCore.Qt.AlignTop)
             widget.setMinimumSize(QSize(500, 500))
 
             self.grid.addWidget(widget, int(self.chartsNum / 2), self.chartsNum % 2)
 
             self.chartsNum += 1
 
+    def deletePlot(self, topic: str):
+        widget = self.widgetDict[topic]
+        self.widgetList.remove(widget)
+        widget.setParent(None)
+
+        del self.widgetDict[topic]
+        del self.canvasDict[topic]
+        del self.figureDict[topic]
+        del self.dataDict[topic]
+
+        self.reorganizePlots()
+
+    def reorganizePlots(self):
+        count = 0
+        for widget in self.widgetList:
+            self.grid.addWidget(widget, int(count / 2), count % 2)
+            count += 1
+
+        self.chartsNum -= 1
+
     def closeEvent(self, a0: QtGui.QCloseEvent) -> None:
         self.worker.stop()
 
@@ -172,6 +198,9 @@ class MainView(QMainWindow):
         self.worker.newMessage.connect(
             lambda message: self.plot(message)
         )
+        self.worker.closeTopic.connect(
+            lambda topic: self.deletePlot(topic)
+        )
         self.worker.window = self
         self.workerThread.start()
 
-- 
GitLab


From e402bc719aaff0467c1f4e3cd7ff9ae3efc4e6ac Mon Sep 17 00:00:00 2001
From: Pavel <murglm@seznam.cz>
Date: Thu, 3 Jun 2021 11:57:31 +0200
Subject: [PATCH 08/12] Re: #8998 - refactoring, comments

---
 aswi2021vochomurka/view/logger_view.py | 14 +++++
 aswi2021vochomurka/view/main_view.py   | 77 +++++++++++++++++++++++---
 aswi2021vochomurka/view/settings.py    | 19 +++++++
 3 files changed, 101 insertions(+), 9 deletions(-)

diff --git a/aswi2021vochomurka/view/logger_view.py b/aswi2021vochomurka/view/logger_view.py
index 2ed4231..0649bc2 100644
--- a/aswi2021vochomurka/view/logger_view.py
+++ b/aswi2021vochomurka/view/logger_view.py
@@ -5,9 +5,15 @@ from PyQt5.QtWidgets import QPlainTextEdit
 
 
 class LoggerView(logging.Handler, QObject):
+    """
+    LoggerView represents console in gui application.
+    """
     append = pyqtSignal(str)
 
     def __init__(self, parent):
+        """
+        Constructor
+        """
         super().__init__()
         super(QObject, self).__init__()
 
@@ -19,10 +25,18 @@ class LoggerView(logging.Handler, QObject):
         )
 
     def emit(self, record):
+        """
+        Emit message from record
+        :param record: record
+        """
         msg = self.format(record)
         self.append.emit(msg)
 
     def appendMessage(self, msg):
+        """
+        Append message
+        :param msg: message
+        """
         self.widget.appendPlainText(msg)
         self.widget.verticalScrollBar().setValue(self.widget.verticalScrollBar().maximum())
 
diff --git a/aswi2021vochomurka/view/main_view.py b/aswi2021vochomurka/view/main_view.py
index 4b86d1b..227444e 100644
--- a/aswi2021vochomurka/view/main_view.py
+++ b/aswi2021vochomurka/view/main_view.py
@@ -19,6 +19,9 @@ from aswi2021vochomurka.view.settings import SettingsDialog, DEFAULT_HOST, DEFAU
 
 
 class Worker(QObject, SubscriberCallback):
+    """
+    Worker representing thread
+    """
     connected = pyqtSignal()
     disconnected = pyqtSignal()
     error = pyqtSignal(Exception)
@@ -28,62 +31,83 @@ class Worker(QObject, SubscriberCallback):
     params: SubscriberParams
 
     def __init__(self, params: SubscriberParams) -> None:
+        """
+        Constructor
+        """
         super().__init__()
         self.params = params
 
     def start(self):
+        """
+        Start worker
+        """
         self.subscriber = MQTTSubscriber(self, self.params)
         self.subscriber.start()
 
     def stop(self):
+        """
+        Stop worker
+        """
         self.subscriber.stop()
 
     def onConnected(self):
+        """
+        Emit connection signal
+        """
         self.connected.emit()
 
     def onDisconnected(self):
+        """
+        Emit disconnection signal
+        """
         self.disconnected.emit()
 
     def onError(self):
         pass
 
     def onMessage(self, message: Message):
+        """
+        Emit message signal
+        :param message: message
+        """
         self.newMessage.emit(message)
 
     def onCloseTopic(self, topic: str):
+        """
+        Emit close topic signal
+        :param topic: topic
+        """
         print("Close topic")
         self.closeTopic.emit(topic)
-        #pass
 
 
 class MainView(QMainWindow):
+    """
+    Main window of application.
+    Displays all parts of the application.
+    """
     worker: Worker = None
     workerThread: QThread = None
 
     def __init__(self):
+        """
+        Constructor - displays all parts of the application.
+        """
         super(MainView, self).__init__()
 
         self.chartsNum = 0
-        self.arrayData = []
-
-        self.dataIndex = 0
         self.dataDict = {}
         self.canvasDict = {}
         self.figureDict = {}
         self.widgetDict = {}
-
         self.widgetList = []
 
-        # self.toolbar = NavigationToolbar(self.canvas, self)
-
-        # self.setMinimumSize(QSize(440, 240))
         self.setMinimumSize(QSize(1200, 800))
         self.setWindowTitle("MQTT client")
 
         logger = self._createLoggerView()
         layout = QVBoxLayout()
         layout.addWidget(logger.widget)
-        # layout.addWidget(self.toolbar)
 
         widget = QWidget()
         widget.setLayout(layout)
@@ -100,6 +124,9 @@ class MainView(QMainWindow):
         self.init_subscriber()
 
     def _createLoggerView(self):
+        """
+        Create logger view
+        """
         logger = LoggerView(self)
         formatter = logging.Formatter('%(asctime)s %(message)s', '%H:%M')
         logger.setFormatter(formatter)
@@ -108,6 +135,9 @@ class MainView(QMainWindow):
         return logger
 
     def _createMenuBar(self):
+        """
+        Creates menu bar
+        """
         menuBar = QMenuBar(self)
         settingsAction = QAction("&Settings", self)
         settingsAction.triggered.connect(self.settings)
@@ -115,7 +145,12 @@ class MainView(QMainWindow):
         self.setMenuBar(menuBar)
 
     def plot(self, message: Message):
+        """
+        Plots new charts or updates old ones
+        :param message: message
+        """
         if message.topic in self.dataDict:
+            # topic already exists
             self.dataDict[message.topic].append(message.value)
 
             figure = self.figureDict[message.topic]
@@ -127,6 +162,7 @@ class MainView(QMainWindow):
 
             self.canvasDict[message.topic].draw()
         else:
+            # new topic
             self.dataDict[message.topic] = [message.value]
 
             figure = plt.figure(figsize=[500, 500])
@@ -155,6 +191,10 @@ class MainView(QMainWindow):
             self.chartsNum += 1
 
     def deletePlot(self, topic: str):
+        """
+        Deletes plot
+        :param topic: topic
+        """
         widget = self.widgetDict[topic]
         self.widgetList.remove(widget)
         widget.setParent(None)
@@ -167,6 +207,9 @@ class MainView(QMainWindow):
         self.reorganizePlots()
 
     def reorganizePlots(self):
+        """
+        Reorganize plots
+        """
         count = 0
         for widget in self.widgetList:
             self.grid.addWidget(widget, int(count / 2), count % 2)
@@ -178,21 +221,33 @@ class MainView(QMainWindow):
         self.worker.stop()
 
     def settings(self):
+        """
+        Opens settings dialog
+        """
         dialog = SettingsDialog()
         if dialog.exec_():
             self.reconnect()
 
     def disconnect(self):
+        """
+        Disconnect
+        """
         self.worker.stop()
         self.workerThread.quit()
         self.workerThread.wait()
 
     def reconnect(self):
+        """
+        Reconnect
+        """
         self.disconnect()
         self.worker.params = self.getConfigParams()
         self.workerThread.start()
 
     def init_subscriber(self):
+        """
+        Initialization of subscriber
+        """
         self.workerThread = QThread()
         self.worker = Worker(self.getConfigParams())
         self.worker.moveToThread(self.workerThread)
@@ -207,6 +262,10 @@ class MainView(QMainWindow):
         self.workerThread.start()
 
     def getConfigParams(self) -> SubscriberParams:
+        """
+        Returns config parameters
+        :return: config parameters
+        """
         settings = get_settings()
 
         connection = ConnectionParams(
diff --git a/aswi2021vochomurka/view/settings.py b/aswi2021vochomurka/view/settings.py
index f76d693..e6e2153 100644
--- a/aswi2021vochomurka/view/settings.py
+++ b/aswi2021vochomurka/view/settings.py
@@ -19,9 +19,16 @@ def get_settings():
 
 
 class SettingsDialog(QDialog):
+    """
+    Settings dialog.
+    In settings dialog is possible to change settings of application.
+    """
     topics = DEFAULT_TOPICS
 
     def __init__(self):
+        """
+        Constructor
+        """
         super(SettingsDialog, self).__init__(None,
                                              QtCore.Qt.WindowCloseButtonHint | QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowTitleHint)
         self.settings = get_settings()
@@ -96,20 +103,32 @@ class SettingsDialog(QDialog):
         self.setLayout(mainLayout)
 
     def addTopic(self):
+        """
+        Add topic
+        """
         item = QListWidgetItem()
         item.setText("/topic")
         item.setFlags(item.flags() | QtCore.Qt.ItemIsEditable)
         self.topicsListWidget.addItem(item)
 
     def removeTopic(self):
+        """
+        Remove topic
+        """
         for item in self.topicsListWidget.selectedItems():
             self.topicsListWidget.takeItem(self.topicsListWidget.row(item))
 
     def anonymousChanged(self):
+        """
+        Changing anonymous/user status
+        """
         self.usernameInput.setEnabled(not self.anonymousInput.isChecked())
         self.passwordInput.setEnabled(not self.anonymousInput.isChecked())
 
     def accept(self) -> None:
+        """
+        Accept changes
+        """
         super().accept()
         self.topics = []
         for index in range(self.topicsListWidget.count()):
-- 
GitLab


From 4e32720c141076f723413d7cd05cb2a69e8500fb Mon Sep 17 00:00:00 2001
From: Jan Rach <rachj@students.zcu.cz>
Date: Thu, 3 Jun 2021 17:15:42 +0200
Subject: [PATCH 09/12] Re: #8968 - second function plotting implemented

---
 aswi2021vochomurka/view/main_view.py | 38 ++++++++++++++++++++++++++--
 1 file changed, 36 insertions(+), 2 deletions(-)

diff --git a/aswi2021vochomurka/view/main_view.py b/aswi2021vochomurka/view/main_view.py
index 227444e..011abdb 100644
--- a/aswi2021vochomurka/view/main_view.py
+++ b/aswi2021vochomurka/view/main_view.py
@@ -4,7 +4,7 @@ import matplotlib.pyplot as plt
 from PyQt5 import QtCore
 from PyQt5 import QtGui
 from PyQt5.QtCore import QSize, QThread, QObject, pyqtSignal, QSettings
-from PyQt5.QtWidgets import QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QScrollArea, QGridLayout
+from PyQt5.QtWidgets import QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QScrollArea, QGridLayout, QFileDialog
 from PyQt5.QtWidgets import QMenuBar, QAction, QPushButton
 from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
 
@@ -97,6 +97,7 @@ class MainView(QMainWindow):
 
         self.chartsNum = 0
         self.dataDict = {}
+        self.dataDict2 = {}
         self.canvasDict = {}
         self.figureDict = {}
         self.widgetDict = {}
@@ -160,6 +161,9 @@ class MainView(QMainWindow):
             figure.suptitle(message.topic)
             plt.plot(self.dataDict[message.topic])
 
+            if message.topic in self.dataDict2:
+                plt.plot(self.dataDict2[message.topic])
+
             self.canvasDict[message.topic].draw()
         else:
             # new topic
@@ -179,8 +183,10 @@ class MainView(QMainWindow):
             self.widgetDict[message.topic] = widget
             self.widgetList.append(widget)
             widget.setLayout(self.layout)
-            button = QPushButton(':')
+            button = QPushButton('Load')
+            button.clicked.connect(lambda: self.getFile(message.topic))
             button.setFixedSize(QSize(40, 40))
+
             self.layout.addWidget(canvas)
             self.layout.addWidget(button)
             self.layout.setAlignment(button, QtCore.Qt.AlignTop)
@@ -190,6 +196,34 @@ class MainView(QMainWindow):
 
             self.chartsNum += 1
 
+    def getFile(self, topic: str):
+        fname = QFileDialog.getOpenFileName(self, 'Open file',
+                                            'c:\\', "CSV files (*.csv)")
+        try:
+            figure = self.figureDict[topic]
+            figure.clear()
+
+            self.dataDict2[topic] = []
+
+            file1 = open(fname[0], 'r')
+            lines = file1.readlines()
+
+            count = 0
+            for line in lines:
+                count += 1
+                parts = line.split(';')
+                value = float(parts[3])
+                self.dataDict2[topic].append(value)
+
+            figure = plt.figure(figure.number)
+            figure.suptitle(topic)
+            plt.plot(self.dataDict[topic])
+            plt.plot(self.dataDict2[topic])
+
+            self.canvasDict[topic].draw()
+        except:
+            print("Loading error")
+
     def deletePlot(self, topic: str):
         """
         Deletes plot
-- 
GitLab


From e91c91d86b4845707091cc46a0c722af65ae91e2 Mon Sep 17 00:00:00 2001
From: Rach <jrach@GK-DOMAIN>
Date: Thu, 3 Jun 2021 17:30:20 +0200
Subject: [PATCH 10/12] Re: #8968 - logging error

---
 aswi2021vochomurka/view/main_view.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/aswi2021vochomurka/view/main_view.py b/aswi2021vochomurka/view/main_view.py
index 011abdb..c2db28c 100644
--- a/aswi2021vochomurka/view/main_view.py
+++ b/aswi2021vochomurka/view/main_view.py
@@ -222,7 +222,7 @@ class MainView(QMainWindow):
 
             self.canvasDict[topic].draw()
         except:
-            print("Loading error")
+            logging.error("Error while loading and displaying data file")
 
     def deletePlot(self, topic: str):
         """
-- 
GitLab


From e89316f60206baffc2982c4fc5833a96b8e65fcc Mon Sep 17 00:00:00 2001
From: Jan Rach <rachj@students.zcu.cz>
Date: Fri, 4 Jun 2021 09:56:20 +0200
Subject: [PATCH 11/12] Re: #8968 - second function delete feature implemented.

---
 aswi2021vochomurka/view/main_view.py | 40 ++++++++++++++++++++++++++--
 1 file changed, 38 insertions(+), 2 deletions(-)

diff --git a/aswi2021vochomurka/view/main_view.py b/aswi2021vochomurka/view/main_view.py
index c2db28c..a296e50 100644
--- a/aswi2021vochomurka/view/main_view.py
+++ b/aswi2021vochomurka/view/main_view.py
@@ -170,10 +170,12 @@ class MainView(QMainWindow):
             self.dataDict[message.topic] = [message.value]
 
             figure = plt.figure(figsize=[500, 500])
+
             canvas = FigureCanvas(figure)
             self.layout = QHBoxLayout()
 
             plt.plot(self.dataDict[message.topic])
+
             figure.suptitle(message.topic)
 
             self.canvasDict[message.topic] = canvas
@@ -187,9 +189,18 @@ class MainView(QMainWindow):
             button.clicked.connect(lambda: self.getFile(message.topic))
             button.setFixedSize(QSize(40, 40))
 
+            button2 = QPushButton('Del')
+            button2.clicked.connect(lambda: self.deleteSecond(message.topic))
+            button2.setFixedSize(QSize(40, 40))
+
             self.layout.addWidget(canvas)
-            self.layout.addWidget(button)
-            self.layout.setAlignment(button, QtCore.Qt.AlignTop)
+
+            boxLayout = QVBoxLayout()
+            boxLayout.addWidget(button)
+            boxLayout.addWidget(button2)
+            boxLayout.setAlignment(button, QtCore.Qt.AlignTop)
+            boxLayout.setAlignment(button2, QtCore.Qt.AlignTop)
+            self.layout.addLayout(boxLayout)
             widget.setMinimumSize(QSize(500, 500))
 
             self.grid.addWidget(widget, int(self.chartsNum / 2), self.chartsNum % 2)
@@ -224,6 +235,19 @@ class MainView(QMainWindow):
         except:
             logging.error("Error while loading and displaying data file")
 
+    def deleteSecond(self, topic: str):
+        if topic in self.dataDict2:
+            del self.dataDict2[topic]
+
+            figure = self.figureDict[topic]
+            figure.clear()
+
+            figure = plt.figure(figure.number)
+            figure.suptitle(topic)
+            plt.plot(self.dataDict[topic])
+
+            self.canvasDict[topic].draw()
+
     def deletePlot(self, topic: str):
         """
         Deletes plot
@@ -237,6 +261,8 @@ class MainView(QMainWindow):
         del self.canvasDict[topic]
         del self.figureDict[topic]
         del self.dataDict[topic]
+        if topic in self.dataDict2:
+            del self.dataDict2[topic]
 
         self.reorganizePlots()
 
@@ -274,6 +300,16 @@ class MainView(QMainWindow):
         """
         Reconnect
         """
+        for widget in self.widgetDict.values():
+            widget.setParent(None)
+
+        self.widgetDict.clear()
+        self.widgetList.clear()
+        self.canvasDict.clear()
+        self.figureDict.clear()
+        self.dataDict.clear()
+        self.dataDict2.clear()
+
         self.disconnect()
         self.worker.params = self.getConfigParams()
         self.workerThread.start()
-- 
GitLab


From 068e18695fc34545ed1996ebcbb38d609d32c36f Mon Sep 17 00:00:00 2001
From: Martin Forejt <mforejt@students.zcu.cz>
Date: Fri, 4 Jun 2021 11:26:38 +0000
Subject: [PATCH 12/12] Feature/build

---
 README.md                 | 17 +++++++++++++++++
 aswi2021vochomurka/app.py |  4 ++++
 pyproject.toml            |  2 +-
 3 files changed, 22 insertions(+), 1 deletion(-)

diff --git a/README.md b/README.md
index 7b2010c..109b679 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,18 @@
 # Konfigurovatelný dashboard zobrazování senzorových dat (KIV) - Vochomůrka
+
+# Build
+```
+poetry install
+poetry build
+```
+```aswi2021vochomurka-1.0.0-py3-none-any.whl``` deployable file will be generated at dist folder
+
+# Install
+```
+pip install aswi2021vochomurka-1.0.0-py3-none-any.whl
+```
+
+# Run
+```
+python -m aswi2021vochomurka.main
+```
\ No newline at end of file
diff --git a/aswi2021vochomurka/app.py b/aswi2021vochomurka/app.py
index 6068b0c..f495985 100644
--- a/aswi2021vochomurka/app.py
+++ b/aswi2021vochomurka/app.py
@@ -1,4 +1,5 @@
 import logging
+import os
 
 from PyQt5.QtCore import QSettings, QCoreApplication
 from PyQt5.QtWidgets import QApplication
@@ -17,6 +18,9 @@ class Application(QApplication):
 
 
 def init_logger():
+    if not os.path.exists('data'):
+        os.mkdir('data')
+
     logging.basicConfig(
         level=logging.DEBUG,
         filename='data/app.log',
diff --git a/pyproject.toml b/pyproject.toml
index 34d46a8..7171e41 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
 [tool.poetry]
 name = "aswi2021vochomurka"
-version = "0.1.0"
+version = "1.0.0"
 description = ""
 authors = ["Tym Vochomurka"]
 
-- 
GitLab