commit 64ca87d0e3c9ddfbb95105e7a4e8e49b23e6be51
Author: Martin Forejt <mforejt@students.zcu.cz>
Date:   Sat Jun 5 22:32:25 2021 +0000

    Develop

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 ea63143..f495985 100644
--- a/aswi2021vochomurka/app.py
+++ b/aswi2021vochomurka/app.py
@@ -1,5 +1,7 @@
 import logging
+import os
 
+from PyQt5.QtCore import QSettings, QCoreApplication
 from PyQt5.QtWidgets import QApplication
 
 from aswi2021vochomurka.view.main_view import MainView
@@ -8,6 +10,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()
@@ -15,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',
@@ -31,3 +37,8 @@ def init_logger():
 
     logging.getLogger('apscheduler').setLevel(logging.WARNING)
     logging.getLogger('matplotlib').setLevel(logging.WARNING)
+
+
+def init_settings():
+    QSettings.setDefaultFormat(QSettings.IniFormat)
+    QSettings.setPath(QSettings.IniFormat, QSettings.SystemScope, '.')
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 be7b979..3466137 100644
--- a/aswi2021vochomurka/service/mqtt/mqtt_subscriber.py
+++ b/aswi2021vochomurka/service/mqtt/mqtt_subscriber.py
@@ -7,9 +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()
 
@@ -19,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))
@@ -33,13 +43,21 @@ 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
         client.on_connect = self.on_connect
         client.on_message = self.on_message
         client.on_disconnect = self.on_disconnect
@@ -47,7 +65,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 +80,14 @@ class MQTTSubscriber(Subscriber):
             return
 
         client.loop_forever()
+
+    def stop(self):
+        """
+        Stop mqtt client
+        """
+        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..82ad578 100644
--- a/aswi2021vochomurka/service/subscriber.py
+++ b/aswi2021vochomurka/service/subscriber.py
@@ -1,40 +1,65 @@
 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:
+    """
+    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
 
-    scheduler = BackgroundScheduler()
+    scheduler: BackgroundScheduler
     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):
-        self.scheduler.shutdown()
+        """
+        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 = {}
+        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)
@@ -44,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
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/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 a769968..a296e50 100644
--- a/aswi2021vochomurka/view/main_view.py
+++ b/aswi2021vochomurka/view/main_view.py
@@ -1,130 +1,356 @@
 import logging
-import math
-import random
 
-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
+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, QFileDialog
+from PyQt5.QtWidgets import QMenuBar, QAction, QPushButton
+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
+from aswi2021vochomurka.view.settings import SettingsDialog, DEFAULT_HOST, DEFAULT_PORT, DEFAULT_KEEPALIVE, \
+    DEFAULT_ANONYMOUS, DEFAULT_USERNAME, DEFAULT_TIMEOUT, DEFAULT_TOPICS, get_settings
 
 
 class Worker(QObject, SubscriberCallback):
+    """
+    Worker representing thread
+    """
     connected = pyqtSignal()
     disconnected = pyqtSignal()
     error = pyqtSignal(Exception)
-    newMessage = pyqtSignal(str)
+    newMessage = pyqtSignal(Message)
+    closeTopic = pyqtSignal(str)
     subscriber: Subscriber = None
+    params: SubscriberParams
 
-    params = SubscriberParams(
-        ["/home/1", "/home/2"],
-        10,
-        ConnectionParams("localhost", 1883, 60),
-        True
-    )
+    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):
-        self.error.emit()
+        pass
 
     def onMessage(self, message: Message):
-        self.newMessage.emit(message.topic)
-        self.window.plot(message)
+        """
+        Emit message signal
+        :param message: message
+        """
+        self.newMessage.emit(message)
 
     def onCloseTopic(self, topic: str):
-        pass
-
-
-class MainView(QDialog):
+        """
+        Emit close topic signal
+        :param topic: topic
+        """
+        print("Close topic")
+        self.closeTopic.emit(topic)
+
+
+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.dataDict2 = {}
+        self.canvasDict = {}
+        self.figureDict = {}
+        self.widgetDict = {}
+        self.widgetList = []
 
-        self.figure = plt.figure(figsize=([500,500]))
+        self.setMinimumSize(QSize(1200, 800))
+        self.setWindowTitle("MQTT client")
 
-        self.canvas = FigureCanvas(self.figure)
-        self.toolbar = NavigationToolbar(self.canvas, self)
+        logger = self._createLoggerView()
+        layout = QVBoxLayout()
+        layout.addWidget(logger.widget)
+
+        widget = QWidget()
+        widget.setLayout(layout)
+        self.setCentralWidget(widget)
+        self._createMenuBar()
 
-        self.setMinimumSize(QSize(440, 240))
-        self.setWindowTitle("MQTT demo")
+        scrollArea = QScrollArea(self)
+        scrollArea.setWidgetResizable(True)
+        scrollContent = QWidget()
+        self.grid = QGridLayout(scrollContent)
+        scrollArea.setWidget(scrollContent)
+        layout.addWidget(scrollArea)
 
-        # Add logger text field
+        self.init_subscriber()
+
+    def _createLoggerView(self):
+        """
+        Create logger view
+        """
         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):
+        """
+        Creates menu bar
+        """
+        menuBar = QMenuBar(self)
+        settingsAction = QAction("&Settings", self)
+        settingsAction.triggered.connect(self.settings)
+        menuBar.addAction(settingsAction)
+        self.setMenuBar(menuBar)
 
-        layout = QVBoxLayout()
-        layout.addWidget(logger.widget)
-        layout.addWidget(self.toolbar)
-        layout.addWidget(self.canvas)
+    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)
 
-        self.setLayout(layout)
+            figure = self.figureDict[message.topic]
+            figure.clear()
 
-        self.initSubscriber()
+            figure = plt.figure(figure.number)
+            figure.suptitle(message.topic)
+            plt.plot(self.dataDict[message.topic])
 
-    def plot(self, message: Message):
-        self.figure.clear()
+            if message.topic in self.dataDict2:
+                plt.plot(self.dataDict2[message.topic])
 
-        if message.topic in self.dataDict:
-            self.dataDict[message.topic].append(message.value)
+            self.canvasDict[message.topic].draw()
         else:
+            # new topic
             self.dataDict[message.topic] = [message.value]
-            self.chartsNum += 1
 
-        rows = math.ceil(self.chartsNum / 2)
+            figure = plt.figure(figsize=[500, 500])
+
+            canvas = FigureCanvas(figure)
+            self.layout = QHBoxLayout()
+
+            plt.plot(self.dataDict[message.topic])
 
-        b = 0
-        for a in self.dataDict.values():
-            self.figure.add_subplot(rows, 2, b + 1)
-            b += 1
-            plt.plot(a)
+            figure.suptitle(message.topic)
 
-        self.canvas.draw()
+            self.canvasDict[message.topic] = canvas
+            self.figureDict[message.topic] = figure
 
-    def initSubscriber(self):
+            widget = QWidget()
+            self.widgetDict[message.topic] = widget
+            self.widgetList.append(widget)
+            widget.setLayout(self.layout)
+            button = QPushButton('Load')
+            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)
+
+            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)
+
+            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:
+            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
+        :param topic: topic
+        """
+        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]
+        if topic in self.dataDict2:
+            del self.dataDict2[topic]
+
+        self.reorganizePlots()
+
+    def reorganizePlots(self):
+        """
+        Reorganize plots
+        """
+        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()
+
+    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
+        """
+        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()
+
+    def init_subscriber(self):
+        """
+        Initialization of subscriber
+        """
         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(
-        #    lambda message: self.b.insertPlainText(message + "\n")
-        # )
+        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()
+
+    def getConfigParams(self) -> SubscriberParams:
+        """
+        Returns config parameters
+        :return: config parameters
+        """
+        settings = get_settings()
+
+        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, int),
+            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..e6e2153
--- /dev/null
+++ b/aswi2021vochomurka/view/settings.py
@@ -0,0 +1,144 @@
+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
+
+
+def get_settings():
+    settings = QSettings('settings.ini', QSettings.IniFormat)
+    return 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()
+        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):
+        """
+        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()):
+            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())
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"]
 
