Exemplo n.º 1
0
def testGetIPOnTarget(base_fixture, mocker):
    from waqd.base.system import RuntimeSystem

    # TODO this would be really cool in Docker with an actual system
    mock_run_on_target(mocker)
    cur_system = RuntimeSystem()

    ip4_ref = "192.168.1.274"
    ip6_ref = "2001:db8:85a3:8d3:1319:8a2e:370:7348"
    mock_call = mocker.Mock()

    # check only ip4
    mock_call.return_value = ip4_ref.encode("utf-8")
    mocker.patch('subprocess.check_output', mock_call)
    [ip4, ip6] = cur_system.get_ip()
    assert ip4 == ip4_ref
    assert ip6 == ""

    # check only ip6
    mock_call.return_value = ip6_ref.encode("utf-8")
    mocker.patch('subprocess.check_output', mock_call)
    [ip4, ip6] = cur_system.get_ip()
    assert ip4 == ""
    assert ip6 == ip6_ref

    # check both ip4 and ip6
    mock_call.return_value = (ip4_ref + " " + ip6_ref).encode("utf-8")
    mocker.patch('subprocess.check_output', mock_call)
    [ip4, ip6] = cur_system.get_ip()
    assert ip4 == ip4_ref
    assert ip6 == ip6_ref
Exemplo n.º 2
0
def testRestart(base_fixture, mocker):
    from waqd.base.system import RuntimeSystem

    mock_run_on_target(mocker)
    mocker.patch('os.system')
    cur_system = RuntimeSystem()
    cur_system.restart()
    # not ideal: confidence through duplication
    os.system.assert_called_once_with('shutdown -r now')  # pylint: disable=no-member
Exemplo n.º 3
0
def testInitOnNonTarget(base_fixture, mocker):
    mock_run_on_non_target(mocker)

    from waqd.base.system import RuntimeSystem
    # This test expects, that it is NOT run on a Raspberry Pi
    cur_system = RuntimeSystem()
    assert not cur_system.is_target_system
    if platform.system == "Linux":  # works only on linux
        assert not cur_system.platform is None

    [ip4, _] = cur_system.get_ip()
    assert not ip4 is None
    def _call_ow_api(self, command: str) -> Dict[str, Any]:
        """ Call the REST like API of OpenWeatherMap. Return response. """
        if self._disabled:
            return {}
        # wait a little bit for connection
        is_connected = RuntimeSystem().wait_for_network()
        if not is_connected:
            # TODO error message
            return {}
        response = []
        if self._cw_json_file and command == self.CURRENT_WEATHER_BY_CITY_ID_API_CMD:
            response.append(self._cw_json_file)
        elif self._fc_json_file and command == self.FORECAST_BY_CITY_ID_API_CMD:
            response.append(self._fc_json_file)
        else:
            try:
                response = urllib.request.urlretrieve(
                    command.format(cid=self._city_id) +
                    self.API_POSTFIX.format(apikey=self._api_key))
            except Exception as error:
                self._logger.error(
                    f"Can't get current weather for {self._city_id} : {str(error)}"
                )

        if not response or not response or not os.path.exists(response[0]):
            return {}

        with open(response[0]) as response_json:
            return json.load(response_json)
Exemplo n.º 5
0
def testInitOnTarget(base_fixture, mocker):
    # Mocks call to get info of Rpi
    mock_run_on_target(mocker)
    from waqd.base.system import RuntimeSystem

    cur_system = RuntimeSystem()
    assert cur_system.is_target_system
    # Actual string is platform dependent
    assert cur_system.platform == "RASPBERRY_PI_4B"
Exemplo n.º 6
0
def main(settings_path: Optional[Path] = None):
    """
    Main function, calling setup, loading components and safe shutdown.
    :param settings_path: Only used for testing to load a settings file.
    """

    # Create user config dir
    if not config.user_config_dir.exists():
        os.makedirs(config.user_config_dir)

    # System is first, is_target_system is the most basic check
    runtime_system = RuntimeSystem()
    if not runtime_system.is_target_system:
        setup_on_non_target_system()

    # All other classes depend on settings
    if not settings_path:
        settings_path = config.user_config_dir
    settings = Settings(ini_folder=settings_path)

    handle_cmd_args(settings)  # cmd args set Debug level for logger

    # to be able to remote debug as much as possible, this call is being done early
    start_remote_debug()

    Logger(
        output_path=config.user_config_dir)  # singleton, no assigment needed

    comp_ctrl = ComponentController(settings)
    config.comp_ctrl = comp_ctrl
    #if config.DEBUG_LEVEL > 0: # disable startup sound
    #    comp_ctrl.components.tts.say_internal("startup", [WAQD_VERSION])
    display_type = settings.get(DISPLAY_TYPE)

    if display_type in [DISP_TYPE_RPI, DISP_TYPE_WAVESHARE_5_LCD]:
        config.qt_app = qt_app_setup(settings)
        # main_ui must be held in this context, otherwise the gc will destroy the gui
        loading_sequence(comp_ctrl, settings)
        try:
            config.qt_app.exec_()
        except:  # pylint:disable=bare-except
            trace_back = traceback.format_exc()
            Logger().error("Application crashed: \n%s", trace_back)
    elif display_type == DISP_TYPE_WAVESHARE_EPAPER_2_9:
        pass
    elif display_type == DISP_TYPE_HEADLESS:
        comp_ctrl.init_all()
        comp_ctrl._stop_event.wait()

    # unload modules - wait for every thread to quit
    if runtime_system.is_target_system:
        Logger().info("Prepare to exit...")
        if comp_ctrl:
            comp_ctrl.unload_all()
            while not comp_ctrl.all_unloaded:
                time.sleep(.1)
Exemplo n.º 7
0
 def __init__(self,
              components: "ComponentRegistry" = None,
              settings: Settings = None):
     self._comps = components
     self._settings = settings  # TODO remove
     self._logger = Logger()
     self._runtime_system = RuntimeSystem()
     self._reload_forbidden = False  # must be set manually in the child class
     self._disabled = False
     self._ready = True
Exemplo n.º 8
0
def start_remote_debug():
    """ Start remote debugging from level 2 and wait on it from level 3"""
    runtime_system = RuntimeSystem()
    if config.DEBUG_LEVEL > 1 and runtime_system.is_target_system:
        import debugpy  # pylint: disable=import-outside-toplevel
        port = 3003
        debugpy.listen(("0.0.0.0", port))
        if config.DEBUG_LEVEL > 2:
            print("Waiting to attach on port %s", port)
            debugpy.wait_for_client(
            )  # blocks execution until client is attached
Exemplo n.º 9
0
    def _watch_components(self):
        """
        Checks existence of global variable of each module and starts it.
        """
        # check and restart wifi
        if RuntimeSystem().is_target_system:
            RuntimeSystem().check_internet_connection()

        for comp_name in self._components.get_names():
            component = self._components.get(comp_name)
            if not component:
                break
            if issubclass(type(component), CyclicComponent):
                if component.is_ready and not component.is_alive and not component.is_disabled:
                    # call stop, so it will be initialized in the next cycle
                    self._components.stop_component(comp_name)

        for sensor_name in self._components.get_sensors():
            sensor = self._components.get_sensors()[sensor_name]
            if not sensor:
                break
            if isinstance(sensor, CyclicComponent
                          ) and sensor.is_ready and not sensor.is_alive:
                self._components.get_sensors().pop(sensor_name)
Exemplo n.º 10
0
def testMH_Z19(base_fixture, target_mockup_fixture, mocker):
    mock_run_on_non_target(mocker)
    assert not RuntimeSystem().is_target_system
    settings = Settings(base_fixture.testdata_path / "integration")
    sensor = sensors.MH_Z19(settings)

    measure_points = 2
    sensor._co2_impl._values_capacity = measure_points
    time.sleep(1)
    assert sensor.is_alive
    assert sensor.is_ready

    # wait until all measurement points are filled up, so that mean value equals the constant value
    # -> takes too long, every call spawns a new python process tgis takes a few seconds
    time.sleep(sensor.UPDATE_TIME + (measure_points + 1))
    from mh_z19 import CO2
    assert sensor.get_co2() == CO2
Exemplo n.º 11
0
def loading_sequence(comp_ctrl: ComponentController, settings: Settings):
    """
    Load modules with watchdog and display Splashscreen until loading is finished.
    """
    # init speech module - this takes a while, so the output will be effectively heard,
    # when the splash screen is already loading. It is a non-blocking call.

    # start init for all components
    comp_ctrl.init_all()
    app_main_ui = main_ui.WeatherMainUi(comp_ctrl, settings)

    if RuntimeSystem().is_target_system:  # only remove titlebar on RPi
        app_main_ui.setWindowFlags(Qt.CustomizeWindowHint)

    # show splash screen
    splash_screen = SplashScreen()
    splash_screen.show()

    # start gui init in separate qt thread
    thread = QtCore.QThread(app_main_ui)
    thread.started.connect(app_main_ui.init_gui)
    thread.start()

    # wait for finishing loading - processEvents is needed for animations to work (loader)
    loading_minimum_time = 10  # seconds
    start = time.time()
    while (not app_main_ui.ready or not comp_ctrl.all_ready) \
        or (time.time() < start + loading_minimum_time) \
            and config.DEBUG_LEVEL <= 3:
        QtWidgets.QApplication.processEvents()

    # splash screen can be disabled - with fader
    app_main_ui.show()
    fade_length = 1  # second
    fader_widget = FaderWidget(  # pylint: disable=unused-variable
        splash_screen,
        app_main_ui,
        length=fade_length * 1000)
    splash_screen.finish(app_main_ui)
    return app_main_ui
Exemplo n.º 12
0
 def _call_tts(self, text, lang):
     """
     Download tts as mp3 and play with VLC. Blocks execution. To be used in a separate thread.
     """
     # remap, if given as setting
     if lang in LANGS_MAP:
         lang = LANGS_MAP.get(lang)
     try:
         # remove most likeable pitfalls. Is not comprehensive!
         normalized_text = text.replace(' ', '_').replace(
             '.', '_').replace('/', '').replace(',', '_').strip()
         audio_file = self._save_dir / f"{normalized_text}_{lang}.mp3"
         # only download, if file does not exist
         if not audio_file.is_file():
             RuntimeSystem().wait_for_network()
             gtts = gTTS(text, lang=lang)
             gtts.save(audio_file)
         if self._comps:
             self._comps.sound.play(audio_file)
         self._logger.debug("Speech: Finished: %s", text)
     except Exception as error:
         self._logger.warning("Speech: Cannot use text to speech engine.: %s", str(error))
Exemplo n.º 13
0
    def __init__(self, main_ui: "WeatherMainUi",
                 comp_ctrl: ComponentController, settings: Settings):
        super().__init__()

        self._main_ui = main_ui  # reference to main ui instance
        self._comp_ctrl = comp_ctrl
        self._comps = comp_ctrl.components
        self._settings = settings
        self._previous_scaling = self._settings.get(FONT_SCALING)
        self._runtime_system = RuntimeSystem()

        # create qt base objects
        self.setWindowFlags(
            Qt.WindowType(Qt.CustomizeWindowHint | Qt.FramelessWindowHint))

        ui_type = uic.loadUiType(config.base_path / "ui" / "qt" / "options.ui")
        self._ui = ui_type[0]()  # 0th element is always the UI class
        self._ui.setupUi(self)

        self.setGeometry(main_ui.geometry())

        # start fader - variable must be held otherwise gc will claim it
        fader_widget = FaderWidget(main_ui, self)  # pylint: disable=unused-variable

        # set version label
        self._ui.version_label.setText(WAQD_VERSION)

        # set up fix background image
        self._ui.background_label.setPixmap(
            QtGui.QPixmap(str(get_asset_file("gui_base", "background-full"))))

        # display current options
        self.display_options()

        # connect buttons on main tab
        self._ui.ok_button.clicked.connect(self.apply_options)
        self._ui.cancel_button.clicked.connect(self.close_ui)
        self._ui.shutdown_button.clicked.connect(self.call_shutdown)
        self._ui.restart_button.clicked.connect(self.call_restart)
        self._ui.connect_wlan_button.clicked.connect(self.connect_wlan)

        self._ui.lang_cbox.currentTextChanged.connect(
            self._update_language_cbox)
        self._ui.forecast_background_cbox.currentTextChanged.connect(
            self._update_preview_forecast)
        self._ui.interior_background_cbox.currentTextChanged.connect(
            self._update_preview_interior)
        self._ui.font_cbox.currentFontChanged.connect(self._update_font)

        # connect elements on energy saver tab
        self._ui.night_mode_begin_slider.valueChanged.connect(
            self._update_night_mode_slider)
        self._ui.night_mode_begin_slider.sliderMoved.connect(
            self._update_night_mode_slider)

        self._ui.night_mode_end_slider.valueChanged.connect(
            self._update_night_mode_slider)
        self._ui.night_mode_end_slider.sliderMoved.connect(
            self._update_night_mode_slider)

        self._ui.brightness_slider.valueChanged.connect(
            self._update_brightness_slider)
        self._ui.brightness_slider.sliderMoved.connect(
            self._update_brightness_slider)

        self._ui.motion_sensor_enable_toggle.toggled.connect(
            self._update_motion_sensor_enabled)

        # set starting tab to first tab
        self._ui.options_tabs.setCurrentIndex(0)

        # set to normal brightness
        self._comps.display.set_brightness(self._settings.get_int(BRIGHTNESS))

        common.scale_gui_elements(
            self,
            self._settings.get_float(FONT_SCALING) * self.EXTRA_SCALING)

        # initialize splash screen for the closing of the UI and make a screenshot
        self._splash_screen = SplashScreen(background=False)
        # minimal wait to show the button feedback
        time.sleep(0.3)
        self.show()
Exemplo n.º 14
0
class OptionMainUi(QtWidgets.QDialog):
    """ Base class of the options qt ui. Holds the option SubUi element. """
    EXTRA_SCALING = 1.15  # make items bigger in this menu
    # matches strings to seconds in dropdown of timeouts
    TIME_CBOX_VALUES = [5, 30, 120, 600, 1800]  # seconds
    FONT_SCALING_VALUES = [1, 1.2, 1.4]
    DHT_PIN_VALUES = ["Disabled", "4", "8", "22"]

    def __init__(self, main_ui: "WeatherMainUi",
                 comp_ctrl: ComponentController, settings: Settings):
        super().__init__()

        self._main_ui = main_ui  # reference to main ui instance
        self._comp_ctrl = comp_ctrl
        self._comps = comp_ctrl.components
        self._settings = settings
        self._previous_scaling = self._settings.get(FONT_SCALING)
        self._runtime_system = RuntimeSystem()

        # create qt base objects
        self.setWindowFlags(
            Qt.WindowType(Qt.CustomizeWindowHint | Qt.FramelessWindowHint))

        ui_type = uic.loadUiType(config.base_path / "ui" / "qt" / "options.ui")
        self._ui = ui_type[0]()  # 0th element is always the UI class
        self._ui.setupUi(self)

        self.setGeometry(main_ui.geometry())

        # start fader - variable must be held otherwise gc will claim it
        fader_widget = FaderWidget(main_ui, self)  # pylint: disable=unused-variable

        # set version label
        self._ui.version_label.setText(WAQD_VERSION)

        # set up fix background image
        self._ui.background_label.setPixmap(
            QtGui.QPixmap(str(get_asset_file("gui_base", "background-full"))))

        # display current options
        self.display_options()

        # connect buttons on main tab
        self._ui.ok_button.clicked.connect(self.apply_options)
        self._ui.cancel_button.clicked.connect(self.close_ui)
        self._ui.shutdown_button.clicked.connect(self.call_shutdown)
        self._ui.restart_button.clicked.connect(self.call_restart)
        self._ui.connect_wlan_button.clicked.connect(self.connect_wlan)

        self._ui.lang_cbox.currentTextChanged.connect(
            self._update_language_cbox)
        self._ui.forecast_background_cbox.currentTextChanged.connect(
            self._update_preview_forecast)
        self._ui.interior_background_cbox.currentTextChanged.connect(
            self._update_preview_interior)
        self._ui.font_cbox.currentFontChanged.connect(self._update_font)

        # connect elements on energy saver tab
        self._ui.night_mode_begin_slider.valueChanged.connect(
            self._update_night_mode_slider)
        self._ui.night_mode_begin_slider.sliderMoved.connect(
            self._update_night_mode_slider)

        self._ui.night_mode_end_slider.valueChanged.connect(
            self._update_night_mode_slider)
        self._ui.night_mode_end_slider.sliderMoved.connect(
            self._update_night_mode_slider)

        self._ui.brightness_slider.valueChanged.connect(
            self._update_brightness_slider)
        self._ui.brightness_slider.sliderMoved.connect(
            self._update_brightness_slider)

        self._ui.motion_sensor_enable_toggle.toggled.connect(
            self._update_motion_sensor_enabled)

        # set starting tab to first tab
        self._ui.options_tabs.setCurrentIndex(0)

        # set to normal brightness
        self._comps.display.set_brightness(self._settings.get_int(BRIGHTNESS))

        common.scale_gui_elements(
            self,
            self._settings.get_float(FONT_SCALING) * self.EXTRA_SCALING)

        # initialize splash screen for the closing of the UI and make a screenshot
        self._splash_screen = SplashScreen(background=False)
        # minimal wait to show the button feedback
        time.sleep(0.3)
        self.show()

    def display_options(self):
        """ Set all elements to the display the current values and set up sliders """
        settings = self._settings

        # read events
        events_json_path = config.user_config_dir / "events.json"
        if events_json_path.exists():
            with open(events_json_path, "r") as fp:
                events_text = fp.read()
                self._ui.event_config_text_browser.setText(events_text)
        self._ui.night_mode_begin_slider.setMaximum(24)
        self._ui.night_mode_begin_slider.setTickInterval(1)
        self._ui.night_mode_begin_value.setText(
            str(settings.get(NIGHT_MODE_BEGIN)))
        self._ui.night_mode_begin_slider.setSliderPosition(
            settings.get(NIGHT_MODE_BEGIN))

        self._ui.night_mode_end_slider.setMaximum(24)
        self._ui.night_mode_end_slider.setTickInterval(1)
        self._ui.night_mode_end_value.setText(str(
            settings.get(NIGHT_MODE_END)))
        self._ui.night_mode_end_slider.setSliderPosition(
            settings.get(NIGHT_MODE_END))

        self._ui.brightness_slider.setMaximum(100)
        self._ui.brightness_slider.setMinimum(30)
        self._ui.brightness_slider.setTickInterval(5)
        self._ui.brightness_value.setText(str(settings.get(BRIGHTNESS)))
        self._ui.brightness_slider.setSliderPosition(settings.get(BRIGHTNESS))

        self._ui.events_enable_toggle.setChecked(settings.get(EVENTS_ENABLED))
        self._ui.auto_updater_enable_toggle.setChecked(
            settings.get(AUTO_UPDATER_ENABLED))
        self._ui.sensor_logging_enable_toggle.setChecked(
            settings.get(LOG_SENSOR_DATA))
        self._ui.updater_beta_channel_toggle.setChecked(
            settings.get(UPDATER_USER_BETA_CHANNEL))

        # themes
        # set background images
        bgr_path = config.assets_path / "gui_bgrs"
        for bgr_file in bgr_path.iterdir():
            self._ui.interior_background_cbox.addItem(bgr_file.name)
            self._ui.forecast_background_cbox.addItem(bgr_file.name)

        self._ui.interior_background_cbox.setCurrentText(
            settings.get(INTERIOR_BG))
        self._ui.forecast_background_cbox.setCurrentText(
            settings.get(FORECAST_BG))
        self._ui.font_cbox.setCurrentText(settings.get(FONT_NAME))
        # try to get index - font-scaling can be set to anything
        try:
            scaling_index = self.FONT_SCALING_VALUES.index(
                self._previous_scaling)
        except Exception:  # normal
            scaling_index = 1
        self._ui.font_scaling_cbox.setCurrentIndex(scaling_index)

        # hw feature toggles
        self._ui.sound_enable_toggle.setChecked(settings.get(SOUND_ENABLED))
        self._ui.motion_sensor_enable_toggle.setChecked(
            settings.get(MOTION_SENSOR_ENABLED))
        self._ui.ccs811_enable_toggle.setChecked(settings.get(CCS811_ENABLED))
        self._ui.bmp280_enable_toggle.setChecked(settings.get(BMP_280_ENABLED))
        self._ui.bme280_enable_toggle.setChecked(settings.get(BME_280_ENABLED))
        self._ui.mh_z19_enable_toggle.setChecked(settings.get(MH_Z19_ENABLED))

        # set up DHT22 combo box
        self._ui.dht22_pin_cbox.addItems(self.DHT_PIN_VALUES)
        # 0 is not in the list and means it is disabled, so it maps to 'Disabled'
        if str(settings.get(DHT_22_PIN)) not in self.DHT_PIN_VALUES:
            selected_pin = 0
        else:
            selected_pin = self.DHT_PIN_VALUES.index(
                str(settings.get(DHT_22_PIN)))

        self._ui.dht22_pin_cbox.setCurrentIndex(selected_pin)

        # set up display type combo box
        display_dict = {
            DISP_TYPE_RPI: "Org. RPi 7\" display",
            DISP_TYPE_WAVESHARE_5_LCD: "Waveshare touch LCD"
        }
        self._ui.display_type_cbox.setDisabled(True)
        self._ui.display_type_cbox.addItems(display_dict.values())

        try:
            self._ui.display_type_cbox.setCurrentIndex(
                list(display_dict.values()).index(
                    display_dict.get(settings.get(DISPLAY_TYPE))))
            self._ui.day_standby_timeout_cbox.setCurrentIndex(
                self.TIME_CBOX_VALUES.index(settings.get(DAY_STANDBY_TIMEOUT)))
            self._ui.night_standby_timeout_cbox.setCurrentIndex(
                self.TIME_CBOX_VALUES.index(
                    settings.get(NIGHT_STANDBY_TIMEOUT)))
        except Exception:  # leave default
            pass

        # enable / disable standby based on motion sensor
        self._ui.day_standby_timeout_cbox.setEnabled(
            settings.get(MOTION_SENSOR_ENABLED))
        self._ui.night_standby_timeout_cbox.setEnabled(
            settings.get(MOTION_SENSOR_ENABLED))

        # populate location dropdown- only ow for now
        for city in settings.get(OW_CITY_IDS):
            self._ui.location_combo_box.addItem(city)

        # set info labels
        self._ui.system_value.setText(self._runtime_system.platform)
        [ipv4, _] = self._runtime_system.get_ip()
        self._ui.ip_address_value.setText(ipv4)

        self._ui.location_combo_box.setCurrentText(settings.get(LOCATION))
        self._ui.lang_cbox.setCurrentText(settings.get(LANG))

        # set to normal brightness - again, in case it was modified
        self._comps.display.set_brightness(self._settings.get(BRIGHTNESS))

    def close_ui(self):
        """
        Transition back to Main Ui.
        Restart unloaded components and re-init main Gui.
        Shows splashscreen.
        """
        # minimal wait to show the button feedback
        time.sleep(0.3)
        self.hide()
        # this splash screen works only in full screen under RPI - god knows why...
        if self._runtime_system.is_target_system:
            self._splash_screen.showFullScreen()
            self._splash_screen.setFocus()
        else:
            self._splash_screen.show()
        loading_minimum_time = 3  # seconds
        start = time.time()
        while not self._comp_ctrl.all_unloaded or (
                time.time() < start + loading_minimum_time):
            config.qt_app.processEvents()

        self._comp_ctrl.init_all()

        while not self._comp_ctrl.all_ready:
            config.qt_app.processEvents()

        self._main_ui.init_gui()
        start = time.time()
        while not self._main_ui.ready or (time.time() <
                                          start + loading_minimum_time):
            config.qt_app.processEvents()

        self._splash_screen.finish(self._main_ui)

        self.close()

    def apply_options(self):
        """
        Callback of Apply Options button.
        Write back every option and close ui.
        """
        settings = self._settings
        settings.set(LOCATION, self._ui.location_combo_box.currentText())
        settings.set(LANG, self._ui.lang_cbox.currentText())
        settings.set(
            DAY_STANDBY_TIMEOUT, self.TIME_CBOX_VALUES[
                self._ui.day_standby_timeout_cbox.currentIndex()])
        settings.set(
            NIGHT_STANDBY_TIMEOUT, self.TIME_CBOX_VALUES[
                self._ui.night_standby_timeout_cbox.currentIndex()])

        settings.set(EVENTS_ENABLED, self._ui.events_enable_toggle.isChecked())
        settings.set(LOG_SENSOR_DATA,
                     self._ui.sensor_logging_enable_toggle.isChecked())

        settings.set(UPDATER_USER_BETA_CHANNEL,
                     self._ui.updater_beta_channel_toggle.isChecked())
        settings.set(AUTO_UPDATER_ENABLED,
                     self._ui.auto_updater_enable_toggle.isChecked())
        settings.set(SOUND_ENABLED, self._ui.sound_enable_toggle.isChecked())

        settings.set(NIGHT_MODE_BEGIN,
                     self._ui.night_mode_begin_slider.sliderPosition())
        settings.set(NIGHT_MODE_END,
                     self._ui.night_mode_end_slider.sliderPosition())
        settings.set(BRIGHTNESS, self._ui.brightness_slider.sliderPosition())

        settings.set(INTERIOR_BG,
                     self._ui.interior_background_cbox.currentText())
        settings.set(FORECAST_BG,
                     self._ui.forecast_background_cbox.currentText())
        settings.set(FONT_NAME, self._ui.font_cbox.currentText())

        if self._ui.dht22_pin_cbox.currentText(
        ) == self.DHT_PIN_VALUES[0]:  # "Disabled"
            settings.set(DHT_22_PIN, DHT_22_DISABLED)
        else:
            settings.set(DHT_22_PIN,
                         int(self._ui.dht22_pin_cbox.currentText()))
        settings.set(MOTION_SENSOR_ENABLED,
                     self._ui.motion_sensor_enable_toggle.isChecked())
        settings.set(BME_280_ENABLED,
                     self._ui.bme280_enable_toggle.isChecked())
        settings.set(BMP_280_ENABLED,
                     self._ui.bmp280_enable_toggle.isChecked())
        settings.set(MH_Z19_ENABLED, self._ui.mh_z19_enable_toggle.isChecked())
        settings.set(CCS811_ENABLED, self._ui.ccs811_enable_toggle.isChecked())

        font_scaling = self.FONT_SCALING_VALUES[
            self._ui.font_scaling_cbox.currentIndex()]
        self._settings.set(FONT_SCALING, font_scaling)

        # write them to file
        settings.save_all_options()

        self.close_ui()

    def _update_language_cbox(self):
        """ Change language in the current ui on value change. """
        self._settings.set(LANG, self._ui.lang_cbox.currentText())
        common.set_ui_language(config.qt_app, self._settings)
        self._ui.retranslateUi(self)
        self.display_options()  # redraw values

    def _update_font(self):
        font = self._ui.font_cbox.currentFont()
        self.setFont(font)
        config.qt_app.setFont(font)

    def _update_preview_interior(self):
        bgr_path = config.assets_path / "gui_bgrs" / self._ui.interior_background_cbox.currentText(
        )
        self._ui.preview_label.setPixmap(QtGui.QPixmap(str(bgr_path)))

    def _update_preview_forecast(self):
        bgr_path = config.assets_path / "gui_bgrs" / self._ui.forecast_background_cbox.currentText(
        )
        self._ui.preview_label.setPixmap(QtGui.QPixmap(str(bgr_path)))

    def _update_night_mode_slider(self):
        """ Callback to update value shown on night mode time silder. """
        self._ui.night_mode_begin_value.setText(
            str(self._ui.night_mode_begin_slider.sliderPosition()))
        self._ui.night_mode_end_value.setText(
            str(self._ui.night_mode_end_slider.sliderPosition()))

    def _update_brightness_slider(self):
        """ Callback to update value shown on brightness silder. """
        self._ui.brightness_value.setText(
            str(self._ui.brightness_slider.sliderPosition()))
        self._comps.display.set_brightness(
            self._ui.brightness_slider.sliderPosition())

    def _update_motion_sensor_enabled(self):
        """ Callback to disable the timeout checkboxes, if motion sensor is checked. """
        value = bool(self._ui.motion_sensor_enable_toggle.isChecked())
        self._ui.day_standby_timeout_cbox.setEnabled(value)
        self._ui.night_standby_timeout_cbox.setEnabled(value)

    def call_shutdown(self):
        """ Callback to shut down the target system. """
        self._runtime_system.shutdown()
        self.close_ui()

    def call_restart(self):
        """ Callback to restart the target system. """
        self._runtime_system.restart()
        self.close_ui()

    def show_updater_ui(self):
        if self._runtime_system.is_target_system:
            # this is the default updater on RaspberryPi OS
            os.system(
                "sudo apt update")  # TODO this takes a while, but is necessary
            os.system("pi-gpk-update-viewer&")

    def connect_wlan(self):
        ssid_name = "Connect_WAQD"
        msg = QtWidgets.QMessageBox(parent=self)
        msg.setStandardButtons(QtWidgets.QMessageBox.Ok)
        msg.setWindowFlags(Qt.WindowType(Qt.CustomizeWindowHint))
        try:
            check_call(f'sudo wifi-connect -s "{ssid_name}"')
            msg.setIcon(QtWidgets.QMessageBox.Information)
            msg.setWindowTitle("Connect to WLAN")
            msg.setText(
                f"Connect to WLAN '{ssid_name}'' on your phone or pc, where you can select your network and enter your password!"
            )
        except Exception as e:
            msg.setIcon(QtWidgets.QMessageBox.Warning)
            msg.setWindowTitle("Error while opening connection to WLAN")
            msg.setText(f"Cannot start WLAN connection portal: {str(e)}")
        # needed because of CustomizeWindowHint
        msg.move(int((self._main_ui.geometry().width() - self.height()) / 2),
                 int((self._main_ui.geometry().height() - msg.height()) / 2))
        msg.exec_()

    def _cyclic_update(self):
        pass