#!/usr/bin/env python3
# Copyright (c) 2022-2023, Dr Rahim Lakhoo, razman786@gmail.com.
#
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation, either version 3 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with
# this program. If not, see <https://www.gnu.org/licenses/>.
#
import os
import glob
import shutil
import warnings

import cpuinfo
import psutil

from pathlib import Path
from contextlib import contextmanager, suppress

from PySide6 import QtCore
from PySide6.QtCore import Qt, QProcess, QSettings, QSize, Slot, QTimer
from PySide6.QtGui import QAction, QActionGroup, QColor, QIcon, QIntValidator, QPalette, QScreen
from PySide6.QtWidgets import (QApplication, QCheckBox, QDialog, QFormLayout, QLabel, QLineEdit, QMainWindow, QMenu,
                               QMessageBox, QPushButton, QSystemTrayIcon)


warnings.filterwarnings("ignore")


class I8kGui(QMainWindow):
    """I8kGui."""

    ERR_PATH = "/some/where/0"

    def __init__(self):
        """__init__."""
        super(I8kGui, self).__init__()
        self.settings = QSettings()
        self.critical_error = False
        self.left_fan_mode = 0
        self.right_fan_mode = 0
        self.left_fan_mode_map = {}
        self.right_fan_mode_map = {}
        self.ac_power_supply_on = None
        self.power_sysfs = None
        self.scaling_sysfs = None
        self.tdp = None
        self.ht_sysfs = None
        self.num_phy_cores = psutil.cpu_count(logical=False)
        self.coretemps_sysfs = []
        self.is_intel_cpu = True
        self.cpu_num = psutil.cpu_count()
        self.tray_menu = QMenu(self)
        self.thermal_menu_group = QActionGroup(self.tray_menu)
        self.thermal_control_menu = QMenu("Thermal Control N/A")
        self.cpupower = None
        self.p = None
        self.p_smbios = None
        self.p_state_smbios = None
        self.is_i8kmon_active = False
        self.is_dell_fan_control_active = False
        self.is_smbios_active = False
        self.degree_sign = u'\N{DEGREE SIGN}'
        self.use_cpu_ave_freq = False
        self.thermal_modes = {}
        self.p_state = None
        self.smbios_data = []
        self.smbios_info = {}
        self.supported_thermal_modes_data = []
        self.supported_thermal_modes = []
        self.current_thermal_mode_data = []
        self.current_thermal_mode = None
        self.always_on = False
        self.bios_thermal_control_on = False
        self.monitor_interval = 1000
        self.always_on_checkbox = QCheckBox()
        self.thermal_checkbox = QCheckBox()
        self.cpu_ave_freq_checkbox = QCheckBox()
        self.interval_input = QLineEdit()
        self.settings_dialog = QDialog(self)
        self.info_dialog = QDialog(self)
        self.save_button = QPushButton("Save", self.settings_dialog)
        self.smbios_button = QPushButton("SMBIOS Information",
                                         self.info_dialog)
        self.cpu_model = QLabel(cpuinfo.get_cpu_info()['brand_raw'])
        self.cpu_tdp = QLabel("N/A")
        self.i8k_format_version = QLabel("1")
        self.bios_version = QLabel("1")
        self.service_tag = QLabel("N/A")
        self.power_status = QLabel("N/A")
        self.button_status = QLabel("N/A")
        self.fan_mode_menu_item = QAction("")
        self.fan_mode_menu_item.setCheckable(False)
        self.left_fan_thermal_menu_item = QAction("")
        self.left_fan_thermal_menu_item.setCheckable(False)
        self.right_fan_thermal_menu_item = QAction("")
        self.right_fan_thermal_menu_item.setCheckable(False)
        self.i8k_info = {}
        self.monitorTimer = QTimer(self)
        self.monitorTimer.timeout.connect(self.refresh_monitor,
                                          type=Qt.QueuedConnection)
        self.monitorTimer.setInterval(self.monitor_interval)
        self.tray = None
        self.first_load = True
        self.cpu_temp_sensors = []
        self.cpu_sysfs_siblings = []
        self.cpu_siblings = {}
        self.cpu_usage = QAction("CPU Usage:\t")
        self.cpu_mhz = QAction("CPU MHz:\t")
        self.cpu_details = QMenu("Per CPU Core\t")
        self.cpu_turbo = QMenu("CPU Turbo Boost\t")
        self.cpu_turbo_enabled = QAction("CPU Turbo Boost:\tN/A")
        self.cpu_turbo_enabled.setCheckable(False)
        self.min_cpu_freq = QAction("CPU Min. Freq:\t0 MHz")
        self.min_cpu_freq.setCheckable(False)
        self.max_cpu_freq = QAction("CPU Max. Freq:\t0 MHz")
        self.max_cpu_freq.setCheckable(False)
        self.min_cpu_turbo = QAction("CPU Min. Turbo:\t0%")
        self.min_cpu_turbo.setCheckable(False)
        self.max_cpu_turbo = QAction("CPU Max. Turbo:\t0%")
        self.max_cpu_turbo.setCheckable(False)
        self.cpu_freq_list = []
        self.cpu_curr_freqs = []
        self.cpu_governor = QAction("CPU Governor:\t")
        self.cpu_temp = QAction("CPU Temp:\t")
        self.left_fan_rpm = QAction("Left Fan:\t")
        self.right_fan_rpm = QAction("Right Fan:\t")
        self.left_fan_status = QAction("Left Fan Mode:\t")
        self.right_fan_status = QAction("Right Fan Mode:\t")
        # check dependencies
        self.check_prerequisites()
        # load settings
        self.load_settings()
        # create settings window
        self.create_settings_dialog()
        # create information window
        self.create_info_dialog()
        # detect CPU architecture
        self.detect_cpu()
        # build CPU sub menu
        self.build_cpu_menu()
        # load i8kgui system tray
        self.load_sys_tray()

    def close_app(self):
        """close_app. Save settings, stop monitoring and quit.

        :param self:
        """
        self.save_settings()
        if self.monitorTimer.isActive():
            self.monitorTimer.stop()
        self.kill_process()
        app.quit()

    def thermal_error_dialog(
        self,
        cmd='disable',
        msg="Critical Service Error. "
            "Please click 'OK' and enter your password if asked."):
        """thermal_error_dialog.

        :param self:
        :param cmd:
        :param msg:
        """
        # displays an error dialog when services are out of sync
        error_dialog = QMessageBox(QMessageBox.Icon.Critical, "i8kgui", msg)
        error_dialog.setText(msg)
        button = error_dialog.open()
        if button == QMessageBox.Ok:
            # disable bios thermal control (i.e. default is i8kmon)
            if cmd in ['enable', 'disable']:
                self.bios_thermal_control(op=cmd)

    def detect_cpu(self):
        """detect_cpu - find out if HT enabled, create a CPU sibling list and a CPU
        temperature sensor list

        :param self:
        """
        # check if Hyper-Threading (HT) is enabled
        try:
            with open(self.ht_sysfs, 'r') as ht_sys:
                for line in ht_sys:
                    self.cpu_ht_enabled = int(line.strip('\n'))
        except IOError:
            print("Error: detecting CPU HT enabled, using alternative")
            # if HT status not available, assume HT enabled if coretemps are
            # less than CPU count
            try:
                if self.is_intel_cpu:
                    self.cpu_ht_enabled = (
                        psutil.cpu_count() >
                        len(psutil.sensors_temperatures()['coretemp']) - 1)
                else:
                    self.cpu_ht_enabled = (
                        psutil.cpu_count() >
                        len(psutil.sensors_temperatures()['k10temp']) - 1)
            except Exception:
                self.cpu_ht_enabled = False

        self.cpu_sysfs_siblings = [
            f"/sys/devices/system/cpu/cpu{i}/topology/thread_siblings_list"
            for i in range(self.cpu_num)
        ]
        print(f"Found: CPU SMT active? {bool(self.cpu_ht_enabled)}")
        print(f"Found: {self.num_phy_cores} CPU Cores")
        print(f"Found: {self.cpu_num} Logical CPU Cores")
        print(f"Found: {len(self.cpu_sysfs_siblings)} CPU siblings in list")
        try:
            self._create_cpu_siblings_temp_sensors()
        except IOError as e:
            print(f"Error sysfs CPU siblings {e}")

    def _create_cpu_siblings_temp_sensors(self):
        """_create_cpu_siblings_temp_sensors. Create a map of CPU siblings,
        using physical and logical CPU cores. Based on this, create a list of
        possible hardware temperature sensors.

        :param self:
        """
        sysfs_files = []
        for cpu in self.cpu_sysfs_siblings:
            cpu_sysfs = Path(cpu).resolve()
            if cpu_sysfs.is_file():
                with open(cpu_sysfs, 'r') as sysfs:
                    sysfs_files.append(sysfs)
                    pair = None
                    for line in sysfs:
                        if ',' in line:
                            # Intel CPU
                            pair = line.strip('\n').split(',')
                        elif '-' in line:
                            # AMD CPU
                            pair = line.strip('\n').split('-')
                        elif not self.cpu_ht_enabled:
                            # create sibling when HT disabled
                            single = line.strip('\n')
                            pair = [single, single]
                        else:
                            print(f"Error: parsing CPU siblings, line = {line}")
                        if pair:
                            if int(pair[0]) not in self.cpu_siblings:
                                # Adding new physical core
                                self.cpu_siblings[int(pair[0])] = int(pair[1])
                            else:
                                # Adding new logical core
                                self.cpu_siblings[int(pair[1])] = int(pair[0])
        if not self.cpu_siblings:
            self.cpu_siblings[0] = 0
        print(f"Found: CPU siblings: {self.cpu_siblings}")
        # create physical CPU temp sensor list
        for i in range(len(self.cpu_siblings)):
            j = i % self.num_phy_cores
            self.cpu_temp_sensors.append(j)
        print(f"Found: CPU temp sensors {self.cpu_temp_sensors}")

    def sanitise_thermal_modes(self, modes):
        """sanitise_thermal_modes.

        :param modes:
        """
        if "Current Thermal Modes: " in modes:
            modes.remove("Current Thermal Modes: ")
        if "Current Thermal Modes:" in modes:
            modes.remove("Current Thermal Modes:")
        if "Supported Thermal Modes: " in modes:
            modes.remove("Supported Thermal Modes: ")
        if "Supported Thermal Modes:" in modes:
            modes.remove("Supported Thermal Modes:")
        if "" in modes:
            modes.remove("")
        modes = [i.strip() for i in modes]
        s_modes = []
        [s_modes.append(x) for x in modes if x not in s_modes]
        return s_modes

    def load_settings(self):
        """load_settings.

        :param self:
        """
        # load continuous data collection setting
        always_on = str(self.settings.value('always_on'))
        if always_on and always_on.lower() == 'false':
            self.always_on = False
            self.always_on_checkbox.setChecked(False)
            self.always_on_checkbox.setCheckState(Qt.CheckState.Unchecked)
        else:
            self.always_on = True
            self.always_on_checkbox.setChecked(True)
            self.always_on_checkbox.setCheckState(Qt.CheckState.Checked)

        # load monitoring interval setting
        self.monitor_interval = int(str(self.settings.value('interval', 1000)))

        # load average CPU freq. setting
        use_cpu_ave_freq = str(self.settings.value('use_cpu_ave_freq'))
        if use_cpu_ave_freq and use_cpu_ave_freq.lower() == 'true':
            self.use_cpu_ave_freq = True
            self.cpu_ave_freq_checkbox.setChecked(True)
            self.cpu_ave_freq_checkbox.setCheckState(Qt.CheckState.Checked)
        else:
            self.use_cpu_ave_freq = False
            self.cpu_ave_freq_checkbox.setChecked(False)
            self.cpu_ave_freq_checkbox.setCheckState(Qt.CheckState.Unchecked)

        if info := list(self.settings.value('smbios', [])):
            # reconstruct list back into dict preserving order
            self.smbios_info = {
                info[i][0]: info[i][1]
                for i in range(0, len(info))
            }
            self.smbios_button.setDisabled(True)

        # load bios thermal mode setting
        bios_thermal_control_on = str(self.settings.value(
            'bios_thermal_control_on'))

        # load current mode
        self.current_thermal_mode = self.settings.value('current_thermal_mode')

        # load supported thermal modes setting
        supported_thermal_modes = self.settings.value(
            'supported_thermal_modes')

        # setup bios thermal mode setting
        self.bios_thermal_control_on = (
            bios_thermal_control_on is not None
            and bios_thermal_control_on.lower() == 'true'
        )

        # create list of supported thermal modes
        if supported_thermal_modes is None:
            self.supported_thermal_modes = []
        else:
            self.supported_thermal_modes = list(supported_thermal_modes)

        for mode in self.supported_thermal_modes:
            self.thermal_modes[mode] = mode.lower().replace(" ", "-")

        # DEBUG
        # print(f"DEBUG: smbios active = {self.is_smbios_active}, "
        #       f"thermal setting on = {self.bios_thermal_control_on}, "
        #       f"i8kmon on = {self.is_i8kmon_active}")

        # enable/disable i8kmon or libsmbios thermal controls
        # based on available dependencies
        if self.is_smbios_active and self.bios_thermal_control_on and not self.is_i8kmon_active:
            self._switch_thermal_mode("enable bios", 'enable', True)
        elif not self.is_smbios_active and not self.bios_thermal_control_on and self.is_i8kmon_active:
            self._switch_thermal_mode("enable i8k", 'disable', False)
            self.switch_thermal_menu()
        elif self.is_smbios_active and not self.bios_thermal_control_on and not self.is_i8kmon_active:
            self._switch_thermal_mode("enable bios", 'enable', True)
        elif self.is_smbios_active and not self.bios_thermal_control_on:
            self._switch_thermal_mode("enable i8k", 'disable', False)
        elif not self.is_smbios_active and not self.is_i8kmon_active:
            print("Info: dell_smm_hwmon thermal mode only, no i8kmon or libsmbios available")
        elif self.is_smbios_active and self.bios_thermal_control_on and self.is_i8kmon_active:
            self._switch_thermal_mode("enable bios", 'enable', True)
        elif self.is_smbios_active and not self.bios_thermal_control_on and self.is_i8kmon_active:
            self._switch_thermal_mode("enable i8k", 'disable', False)
        elif not self.is_smbios_active and self.bios_thermal_control_on and self.is_i8kmon_active:
            self._switch_thermal_mode("enable i8k", 'disable', False)
            self.switch_thermal_menu()
        else:
            print(f"Error: unknown thermal mode condition - "
                  f"smbios active = {self.is_smbios_active}, "
                  f"thermal setting on = {self.bios_thermal_control_on}, "
                  f"i8kmon on = {self.is_i8kmon_active}")

    def _switch_thermal_mode(self, arg0, cmd, arg2):
        """_switch_thermal_mode.

        :param self:
        :param arg0:
        :param cmd:
        :param arg2:
        """
        print(f"Info: switching thermal mode to {arg0}")
        self.bios_thermal_control(op=cmd)
        self.thermal_checkbox.setChecked(arg2)

    def save_settings(self):
        """save_settings."""
        self.settings.setValue('current_thermal_mode',
                               self.current_thermal_mode)
        self.settings.setValue('supported_thermal_modes',
                               self.supported_thermal_modes)
        self.settings.setValue('bios_thermal_control_on',
                               self.bios_thermal_control_on)
        self.settings.setValue('always_on', self.always_on)
        self.settings.setValue('use_cpu_ave_freq', self.use_cpu_ave_freq)
        self.settings.setValue('interval', self.monitor_interval)
        if self.smbios_info:
            # preserve ordering by storing a list
            self.settings.setValue('smbios', list(self.smbios_info.items()))

    def handle_thermal_control(self, checked):
        """Enable/disable thermal control based on the current state of smbios and i8kmon.

        :param checked: The state of the thermal checkbox.
        """
        if checked != 0:
            if self.is_smbios_active:
                self.enable_bios_thermal_control()
                self.switch_thermal_menu()
                print("Switching to bios thermal control")
            else:
                self.handle_inactive_controller('i8kmon', 'smbios')
        else:
            if self.is_i8kmon_active:
                self.disable_bios_thermal_control()
                self.switch_thermal_menu()
                print("Switching to i8kmon thermal control")
            else:
                self.handle_inactive_controller('smbios', 'i8kmon')

    def handle_inactive_controller(self, active_controller, inactive_controller):
        """Handle the case where the active thermal controller is not available.

        :param active_controller: The name of the active controller.
        :param inactive_controller: The name ofthe inactive controller.
        """
        if inactive_controller == 'smbios':
            self.disable_bios_thermal_control()
        else:
            self.enable_bios_thermal_control()
        self.thermal_checkbox.setChecked(False)
        self.thermal_checkbox.setCheckState(Qt.CheckState.Unchecked)
        self.switch_thermal_menu()
        print(f"{active_controller} is not available, reverting to {inactive_controller} thermal control")
        self.thermal_error_dialog(
            'error', f"{active_controller} is not active. Reverting to {inactive_controller} thermal control")

    def enable_bios_thermal_control(self):
        """Enable thermal control through BIOS."""
        self.bios_thermal_control(op='enable')

    def disable_bios_thermal_control(self):
        """Disable thermal control through BIOS."""
        self.bios_thermal_control(op='disable')

    def always_collect(self, checked):
        """always_collect. Collect metrics continuously at a set time interval.

        :param checked:
        """
        if checked != 0:
            self.monitor_interval = int(self.interval_input.text())
            self.monitorTimer.setInterval(self.monitor_interval)
            self.always_on = True
            if self.monitorTimer.isActive():
                self.monitorTimer.stop()
            self.monitorTimer.start()
        else:
            self.always_on = False
            if self.monitorTimer.isActive():
                self.monitorTimer.stop()

    def enable_ave_cpu_freq(self, checked):
        """enable_ave_cpu_freq. Enable average CPU frequency over all all cores.

        :param checked:
        """
        self.use_cpu_ave_freq = checked != 0

    def save_dialog_settings(self):
        """save_dialog_settings."""
        if self.monitor_interval != int(self.interval_input.text()):
            self.monitor_interval = int(self.interval_input.text())
            self.monitorTimer.setInterval(self.monitor_interval)
            if self.monitorTimer.isActive():
                self.monitorTimer.stop()
                self.monitorTimer.start()
        self.save_settings()
        self.settings_dialog.close()

    def close_info_dialog(self):
        """close_info_dialog.

        :param self:
        """
        self.save_settings()
        self.info_dialog.close()

    def smbios_message(self, msg):
        """smbios_message.

        :param msg:
        """
        split_msg = msg.split(': ')
        for item in split_msg:
            split_item = item.split('\n')
            for part_item in split_item:
                self.smbios_data.append(part_item.strip())

    def check_thermal_control(self):
        """check_thermal_control. Check systemd services.

        :param self:
        """
        active_services = []
        self._is_systemd_service_active(
            'i8kmon.service',
            active_services,
            "Warn: systemctl i8kmon.service not found",
        )
        self._is_systemd_service_active(
            'dell-bios-fan-control.service',
            active_services,
            "Warn: systemctl dell-bios-fan-control.service not found",
        )
        # The i8kmon service needs to be active, otherwise it will
        # revert to BIOS mode, using libsmbios if available.
        # If neither are available then only the kernel driver dell_smm_hwmon is used.
        return active_services

    def _is_systemd_service_active(self, service, active_services, err_msg):
        """_is_systemd_service_active.

        :param self:
        :param service:
        :param active_services:
        :param err_msg:
        """
        # check which system services are active.
        # keep a reference to the QProcess (e.g. on self)
        # while it's running.
        subproc = QProcess()
        subproc.start("systemctl", ['is-enabled', service])
        subproc.waitForFinished(1000)
        result_systemd = subproc.readLine()
        output_systemd = bytes(result_systemd).decode("utf8")
        is_service_active = output_systemd.strip('\n')
        if subproc.state != 0:
            self.kill_process(subproc)
        subproc = None
        # check if systemd service is enabled,
        if is_service_active == 'enabled':
            active_services.append(service)
        else:
            print(f"Error: systemd service check {err_msg}")

    def check_prerequisites(self):
        """check_prerequisites. Check for sysfs, i8k and SMBIOS.

        :param self:
        """
        # detect if AMD or Intel CPU
        cpu_vendor = cpuinfo.get_cpu_info()['vendor_id_raw']
        if cpu_vendor == "GenuineIntel":
            print("Found: Intel CPU")
        elif cpu_vendor == "AuthenticAMD":
            print("Found: AMD CPU")
            self.is_intel_cpu = False
        else:
            print(f"Error: CPU vendor not supported {cpu_vendor}")

        #
        # check Sysfs interfaces
        #

        def check_sysfs(path, found_str, not_found_str):
            if Path(path).is_file():
                print(f"Found: {found_str}")
                return True
            else:
                print(f"Error: {not_found_str} not found")
                return False

        # Check AC power supply
        self.power_sysfs = "/sys/class/power_supply/AC/online"
        if check_sysfs(self.power_sysfs, "Sysfs AC power supply", "Sysfs AC power supply"):
            self.power_enabled = True
        else:
            self.power_sysfs = "/sys/class/power_supply/ACAD/online"
            if check_sysfs(self.power_sysfs, "Sysfs ACAD power supply", "Sysfs AC power supply"):
                self.power_enabled = True
            else:
                self.power_enabled = False

        # Check Scaling Governor
        self.scaling_sysfs = "/sys/devices/system/cpu/cpu0/cpufreq/scaling_governor"
        if check_sysfs(self.scaling_sysfs, "Sysfs scaling governor", "Sysfs scaling governor not found"):
            self.scaling_enabled = True
        else:
            self.scaling_enabled = False

        # Check RAPL TDP
        # not available on AMD Dell 5575
        self.tdp = "/sys/devices/virtual/powercap/intel-rapl/intel-rapl:0/constraint_0_max_power_uw"
        if check_sysfs(self.tdp, "Sysfs RAPL TDP", "Sysfs RAPL TDP not found"):
            self.tdp_enabled = True
        elif check_sysfs(self.tdp, "Sysfs RAPL TDP alternative", "Sysfs RAPL TDP not found"):
            self.tdp_enabled = True
        else:
            self.tdp_enabled = False

        # Check SMT
        self.ht_sysfs = "/sys/devices/system/cpu/smt/active"
        if check_sysfs(self.ht_sysfs, "Sysfs SMT status", "Sysfs SMT status not found"):
            pass

        # Check CPU siblings
        cpu_siblings_sysfs = "/sys/devices/system/cpu/cpu0/topology/thread_siblings_list"
        if check_sysfs(cpu_siblings_sysfs, "Sysfs CPU siblings", "Sysfs CPU siblings not found"):
            pass

        #
        # check CPU coretemps and turbo settings
        #

        # check cpu coretemp or k10temp sysfs interface
        use_hwmon_temp = False
        if self.is_intel_cpu:
            with suppress(Exception):
                print("Found Intel sensors:")
                for temp in psutil.sensors_temperatures()['coretemp']:
                    print(f"Sensor: {temp}")
            try:
                for cpu in range(self.num_phy_cores):
                    self.coretemps_sysfs.append(
                        glob.glob(
                            f"/sys/devices/platform/coretemp.0/hwmon/*/temp{cpu + 1}_input"
                        )[0])
                num_temps = len(self.coretemps_sysfs)
                if num_temps >= 1:
                    print(f"Found: {num_temps} Sysfs Intel coretemps")
                    for t in self.coretemps_sysfs:
                        temp = Path(t).resolve()
                        if not temp.is_file():
                            print(f"Error: Sysfs Intel coretemp {t} is not available")
                else:
                    print("Error: Sysfs Intel coretemps not found")
                    use_hwmon_temp = True
            except Exception:
                print("Error: Sysfs Intel coretemps are not available")
                use_hwmon_temp = True
        else:
            with suppress(Exception):
                print("Found AMD sensors:")
                for temp in psutil.sensors_temperatures()['k10temp']:
                    print(f"Sensor: {temp}")
            # search hwmon for k10temp
            self.k10_hwmon = None
            try:
                hwmon_list = glob.glob("/sys/class/hwmon/hwmon*/name")
            except Exception:
                print("Error: Sysfs hwmon traversal")
            else:
                for hwmon in hwmon_list:
                    with open(hwmon, 'r') as mon:
                        for line in mon:
                            val = line.strip('\n')
                            if val == 'k10temp':
                                self.k10_hwmon = os.path.dirname(hwmon)
                                print(f"Found: AMD k10temp {self.k10_hwmon}")
            # look for Tdie or Tctl in k10temp
            # https://www.kernel.org/doc/html/latest/hwmon/k10temp.html
            if self.k10_hwmon:
                self.k10_cpu_temp = None
                try:
                    k10_mons = glob.glob(f"{self.k10_hwmon}/temp*_label")
                except Exception:
                    print("Error: Sysfs hwmon k10temp")
                else:
                    for temp in k10_mons:
                        with open(temp, 'r') as mon:
                            for line in mon:
                                val = line.strip('\n')
                                if val == 'Tdie':
                                    self.k10_cpu_temp = temp
                                    print(f"Found: AMD k10temp Tdie {temp}")
                                elif val == 'Tctl' and not self.k10_cpu_temp:
                                    self.k10_cpu_temp = temp
                                    print(f"Found: AMD k10temp Tctl {temp}")
                # There are no individual CPU core temps for k10temp
                if self.k10_cpu_temp:
                    self.k10_cpu_temp = self.k10_cpu_temp.replace("label", "input")
                    print(f"Found: AMD k10temp input {self.k10_cpu_temp}")
                    # loop for available cores
                    for _ in range(self.cpu_num):
                        self.coretemps_sysfs.append(self.k10_cpu_temp)
                    print(f"Found: {len(self.coretemps_sysfs)} Sysfs AMD coretemps")
                else:
                    print("Error: Sysfs AMD coretemps are not available")
                    use_hwmon_temp = True

        # check cpu turbo sysfs interfaces
        if self.is_intel_cpu:
            self.cpu_no_turbo = "/sys/devices/system/cpu/intel_pstate/no_turbo"
            if Path(self.cpu_no_turbo).is_file():
                print("Found: Sysfs CPU Turbo")
            else:
                print("Error: Sysfs CPU Turbo not found")

            self.cpu_min_perf = "/sys/devices/system/cpu/intel_pstate/min_perf_pct"
            if Path(self.cpu_min_perf).is_file():
                print("Found: Sysfs CPU min perf")
            else:
                print("Error: Sysfs CPU min perf not found")

            self.cpu_max_perf = "/sys/devices/system/cpu/intel_pstate/max_perf_pct"
            if Path(self.cpu_max_perf).is_file():
                print("Found: Sysfs CPU max perf")
            else:
                print("Error: Sysfs CPU max perf not found")

            self.cpu_min_scaling = "/sys/devices/system/cpu/cpufreq/policy0/scaling_min_freq"
            if Path(self.cpu_min_scaling).is_file():
                print("Found: Sysfs CPU min scaling")
            else:
                print("Error: Sysfs CPU min scaling not found")

            self.cpu_max_scaling = "/sys/devices/system/cpu/cpufreq/policy0/scaling_max_freq"
            if Path(self.cpu_max_scaling).is_file():
                print("Found: Sysfs CPU max scaling")
            else:
                print("Error: Sysfs CPU max scaling not found")
        elif not self.is_intel_cpu:
            self.cpu_no_turbo = "/sys/devices/system/cpu/cpufreq/boost"  # 0 or 1
            if Path(self.cpu_no_turbo).is_file():
                print("Found: Sysfs CPU Turbo")
            else:
                print("Error: Sysfs CPU Turbo not found")

            # amd_pstate does not have a sysfs entry for lowest perf
            # not available on AMD Dell 5575
            self.cpu_min_perf = "/sys/devices/system/cpu/cpu0/acpi_cppc/lowest_nonlinear_perf"
            if Path(self.cpu_min_perf).is_file():
                print("Found: Sysfs CPU min perf")
            else:
                self.cpu_min_perf = "/sys/devices/system/cpu/cpu0/acpi_cppc/lowest_perf"
                if Path(self.cpu_min_perf).is_file():
                    print("Found: Sysfs CPU min perf alternative")
                else:
                    print("Error: Sysfs CPU min perf not found")
                    self.cpu_min_perf = self.ERR_PATH

            # values between acpi_cppc and amd_pstate differ for highest perf
            # sample Ryzen output online shows cpu0 and policy0 to be equal, but the rest differ
            # not available on AMD Dell 5575
            self.cpu_max_perf = "/sys/devices/system/cpu/cpufreq/policy0/amd_pstate_highest_perf"
            if Path(self.cpu_max_perf).is_file():
                print("Found: Sysfs CPU max perf")
            else:
                print("Error: Sysfs CPU max perf not found")
                self.cpu_max_perf = "/sys/devices/system/cpu/cpu0/acpi_cppc/highest_perf"
                if Path(self.cpu_max_perf).is_file():
                    print("Found: Sysfs CPU max perf using alternative")
                else:
                    print("Error: Sysfs CPU max perf alternative not found")
                    self.cpu_min_perf = self.ERR_PATH

            self.cpu_min_scaling = "/sys/devices/system/cpu/cpufreq/policy0/amd_pstate_lowest_nonlinear_freq"
            if Path(self.cpu_min_scaling).is_file():
                print("Found: Sysfs CPU min scaling")
            else:
                # AMD Dell 5575
                self.cpu_min_scaling = "/sys/devices/system/cpu/cpufreq/policy0/scaling_min_freq"
                if Path(self.cpu_min_scaling).is_file():
                    print("Found: Sysfs CPU min scaling using alternative")
                else:
                    print("Error: Sysfs CPU min scaling alternative not found")

            self.cpu_max_scaling = "/sys/devices/system/cpu/cpufreq/policy0/amd_pstate_max_freq"
            if Path(self.cpu_max_scaling).is_file():
                print("Found: Sysfs CPU max scaling")
            else:
                # AMD Dell 5575
                self.cpu_max_scaling = "/sys/devices/system/cpu/cpufreq/policy0/scaling_max_freq"
                if Path(self.cpu_max_scaling).is_file():
                    print("Found: Sysfs CPU max scaling using alternative")
                else:
                    print("Error: Sysfs CPU max scaling not found")
        else:
            print("Error: Sysfs CPU Turbo")

        #
        # check dell_smm_hwmon Sysfs interfaces
        #

        # check dell_smm_hwmon fan1
        try:
            self.i8kmon_fan1 = glob.glob(
                "/sys/module/dell_smm_hwmon/drivers/platform:dell_smm_hwmon/dell_smm_hwmon/hwmon/*/fan1_input"
            )[0]
            self.i8kmon_fan1_target = glob.glob(
                "/sys/module/dell_smm_hwmon/drivers/platform:dell_smm_hwmon/dell_smm_hwmon/hwmon/*/fan1_target"
            )[0]
        except Exception:
            print("Error: dell_smm_hwmon fan1 not found")
            self.i8kmon_fan1 = self.ERR_PATH
            self.i8kmon_fan1_target = self.ERR_PATH
        else:
            if Path(self.i8kmon_fan1).is_file():
                print("Found: dell_smm_hwmon fan1")
                # check dell_smm_hwmon fan1_target
                if Path(self.i8kmon_fan1_target).is_file():
                    print("Found: dell_smm_hwmon fan1_target")
                else:
                    print("Error: dell_smm_hwmon fan1_target not found")

        # check dell_smm_hwmon fan2
        # not available on AMD Dell 5575
        try:
            self.i8kmon_fan2 = glob.glob(
                "/sys/module/dell_smm_hwmon/drivers/platform:dell_smm_hwmon/dell_smm_hwmon/hwmon/*/fan2_input"
            )[0]
            self.i8kmon_fan2_target = glob.glob(
                "/sys/module/dell_smm_hwmon/drivers/platform:dell_smm_hwmon/dell_smm_hwmon/hwmon/*/fan2_target"
            )[0]
        except Exception:
            print("Error: dell_smm_hwmon fan2 not found")
            self.i8kmon_fan2 = self.ERR_PATH
            self.i8kmon_fan2_target = self.ERR_PATH
        else:
            if Path(self.i8kmon_fan2).is_file():
                print("Found: dell_smm_hwmon fan2")
                # check dell_smm_hwmon fan2_target
                if Path(self.i8kmon_fan2_target).is_file():
                    print("Found: dell_smm_hwmon fan2_target")
                else:
                    print("Error: dell_smm_hwmon fan2_target not found")

        # check for dell_smm_hwmon CPU temp
        try:
            self.i8kmon_temp = glob.glob(
                "/sys/module/dell_smm_hwmon/drivers/platform:dell_smm_hwmon/dell_smm_hwmon/hwmon/*/temp1_input"
            )[0]
        except Exception:
            print("Error: dell_smm_hwmon CPU temp not found")
            self.i8kmon_temp = self.ERR_PATH
        else:
            if Path(self.i8kmon_temp).is_file():
                print("Found: dell_smm_hwmon CPU temp")
                if use_hwmon_temp:
                    self.coretemps_sysfs.append(self.i8kmon_temp)
                    print("Warn: Using dell_smm_hwmon for CPU temp")
            else:
                print("Error: dell_smm_hwmon CPU temp not found")

        #
        # check thermal services and scripts
        #

        # check systemd services
        if active_services := self.check_thermal_control():
            for srv in active_services:
                print(f"Found: {srv} is active")
                if srv == 'i8kmon.service':
                    self.is_i8kmon_active = True
                elif srv == 'dell-bios-fan-control.service':
                    self.is_dell_fan_control_active = True
        # check for i8k
        if self.is_i8kmon_active:
            self.i8kmon_ctl = shutil.which("i8kctl")
            if Path(self.i8kmon_ctl).is_file():
                print(f"Found: i8kctl {self.i8kmon_ctl}")
            else:
                print("Error: i8kctl not found")

            # check for i8kmon.conf
            # i8kutils checks for /etc/i8kmon.conf and then
            # ~/.i8kmon. Added additional /etc/i8kutils/i8kmon.conf
            config_file_list = ['/etc/i8kmon.conf', '/etc/i8kutils/i8kmon.conf',
                                f'{str(Path.home())}/.i8kmon']
            for config in config_file_list:
                if Path(config).resolve().is_file():
                    self.i8kmon_conf = config
            if self.i8kmon_conf:
                print(f"Found: i8kmon config file {self.i8kmon_conf}")
            else:
                print("Error: i8kmon config file not found")

        # check for i8kgu thermal control script
        thermal_file = Path(str(shutil.which("i8kgui_thermal_control")))
        if not thermal_file.is_file():
            thermal_file = Path("i8kgui_thermal_control")
        # check for smbios-thermal-ctl
        self.smbois_ctl = Path(str(shutil.which("smbios-thermal-ctl"))).resolve()
        # check that smbios is working
        if self.smbois_ctl.is_file():
            print(f"Found: (SM)BIOS Thermal Control {self.smbois_ctl}")
            subproc = QProcess()
            subproc.start("pkexec", [str(thermal_file.resolve()), '--status'])
            subproc.waitForFinished(1000)
            smbios_stdout = subproc.readAllStandardOutput()
            if subproc.state() != 0:
                self.kill_process(subproc)
            subproc = None
            smbios_stdout = bytes(smbios_stdout).decode("utf8")
            if smbios_stdout.startswith('Status Current Thermal Modes'):
                split_stdout = smbios_stdout.split('\n')
                split_stdout = self.sanitise_thermal_modes(split_stdout)
                for item in split_stdout[1:]:
                    print(f"Found: libsmbios current thermal mode '{item}'")
                    self.current_thermal_mode_data.append(item)
                if self.current_thermal_mode_data:
                    self.is_smbios_active = True  # DEBUG = False
            else:
                print("Error: (SM)BIOS Thermal Control is not functional")
        else:
            print("Error: (SM)BIOS Thermal Control not found")

        #
        # finalise prerequisites
        #

        # check i8kmon prerequisites
        # minimum 1 fan and CPU temp
        i8kmon_passed = bool(
            (
                (Path(self.i8kmon_fan1).is_file()
                    or Path(self.i8kmon_fan2).is_file())
                and (Path(self.i8kmon_fan1_target).is_file()
                     or Path(self.i8kmon_fan2_target).is_file())
                and Path(self.i8kmon_temp).is_file()
            )
        )
        # check sysfs prerequisites
        # minimum scaling, SMT and CPU core siblings
        sysfs_passed = bool(
            (Path(self.scaling_sysfs).is_file()
             and Path(self.ht_sysfs).is_file()
             and Path(cpu_siblings_sysfs).is_file()))
        # majority of the information is taken from the
        # dell_smm_hwmon kernel module
        # minimum Sysfs, i8kmon, i8kctl or smbios
        if sysfs_passed and i8kmon_passed:
            if self.is_i8kmon_active:
                # read config file
                self.get_i8k_information()
            return True
        else:
            # disable menu and return False
            print("Warn: No thermal controls found, disabling menu")
            self.thermal_control_menu.setTitle("Thermal Control N/A")
            self.thermal_menu_group.setDisabled(True)
            return False

    def bios_thermal_control(self, op=None):
        """bios_thermal_control.

        :param op:
        """
        # enable or disable BIOS thermal control
        if self.p is not None:
            return False

        # find i8kgui thermal control script
        thermal_file = Path(str(shutil.which("i8kgui_thermal_control")))
        if not thermal_file.is_file():
            thermal_file = Path("i8kgui_thermal_control")
        # enable bios control and disable i8kmon
        if op == 'enable':
            self.p = QProcess()
            self.p.readyReadStandardOutput.connect(
                self.handle_thermal_stdout, type=Qt.QueuedConnection)
            self.p.readyReadStandardError.connect(
                self.handle_thermal_stderr, type=Qt.QueuedConnection)
            self.p.stateChanged.connect(self.handle_process_state, type=Qt.QueuedConnection)
            # clean up once complete
            self.p.finished.connect(self.thermal_process_finished, type=Qt.QueuedConnection)
            self.p.start("pkexec",
                         [str(thermal_file.resolve()), '--enable'])
        # disable bios control and enable i8kmon
        elif op == 'disable':
            self.p = QProcess()
            self.p.readyReadStandardOutput.connect(
                self.handle_thermal_stdout, type=Qt.QueuedConnection)
            self.p.readyReadStandardError.connect(
                self.handle_thermal_stderr, type=Qt.QueuedConnection)
            self.p.stateChanged.connect(self.handle_process_state, type=Qt.QueuedConnection)
            # clean up once complete
            self.p.finished.connect(self.thermal_process_disable_finished, type=Qt.QueuedConnection)
            self.p.start("pkexec",
                         [str(thermal_file.resolve()), '--disable'])
        # get BIOS controlled thermal mode
        elif op == 'status':
            self.p = QProcess()
            self.p.readyReadStandardOutput.connect(
                self.handle_thermal_stdout, type=Qt.QueuedConnection)
            self.p.readyReadStandardError.connect(
                self.handle_thermal_stderr, type=Qt.QueuedConnection)
            self.p.stateChanged.connect(self.handle_process_state, type=Qt.QueuedConnection)
            # clean up once complete
            self.p.finished.connect(self.thermal_process_finished, type=Qt.QueuedConnection)
            self.p.start("pkexec",
                         [str(thermal_file.resolve()), '--status'])

    def handle_thermal_stderr(self):
        """handle_thermal_stderr.

        :param self:
        """
        if self.p:
            data = self.p.readAllStandardError()
            stderr = bytes(data).decode("utf8")
            # check if pkexec passwd input cancelled
            if 'Request dismissed' in stderr:
                self.p.finished.emit(1, QProcess.CrashExit)
            else:
                print(f"Error: handle_thermal_stderr {stderr}")

    def handle_thermal_stdout(self):
        """handle_thermal_stdout.

        :param self:
        """
        if not self.p:
            return

        data = self.p.readAllStandardOutput()
        stdout = bytes(data).decode("utf8")

        # get supported BIOS thermal modes
        if stdout.startswith('Supported Thermal Modes'):
            split_stdout = stdout.split('\n')
            split_stdout = self.sanitise_thermal_modes(split_stdout)
            for item in split_stdout:
                self.supported_thermal_modes_data.append(item)
        # get current thermal mode
        elif stdout.startswith('Current Thermal Modes'):
            split_stdout = stdout.split('\n')
            split_stdout = self.sanitise_thermal_modes(split_stdout)
            for item in split_stdout:
                self.current_thermal_mode_data.append(item)
        # get return from successful change of mode
        elif stdout.startswith('Helper function to Set Thermal Mode'):
            split_stdout = stdout.split(': ')
            self.current_thermal_mode = split_stdout[1].strip().title(
            ).replace("-", " ")
        elif stdout.startswith('Status Current Thermal Modes'):
            print(f"stdout {stdout}")
            split_stdout = stdout.split('\n')
            split_stdout = self.sanitise_thermal_modes(split_stdout)
            for item in split_stdout:
                self.current_thermal_mode_data.append(item)

    def close_process(self, proc=None):
        """close_process.

        :param self:
        """
        if proc is None and self.p:
            if self.p.state() != 0:
                with suppress(Exception):
                    self.p.terminate()
                    self.p.waitForFinished(1000)
                    self.p.kill()
            self.p = None
        elif proc:
            if proc.state() != 0:
                with suppress(Exception):
                    proc.terminate()
                    proc.waitForFinished(1000)
                    proc.kill()
            proc = None

    def kill_process(self, proc=None):
        """kill_process.

        :param self:
        """
        if proc is None and self.p:
            with suppress(Exception):
                self.p.terminate()
                self.p.waitForFinished(1000)
                self.p.kill()
            self.p = None
        elif proc:
            with suppress(Exception):
                proc.terminate()
                proc.waitForFinished(1000)
                proc.kill()
            proc = None

    def thermal_selection_finished(self):
        """thermal_selection_finished.

        :param self:
        """
        # bios mode change
        if self.p and self.p.exitCode() != 0:
            self.kill_process()
            return
        else:
            self.close_process()
            self.update_thermal_modes()

    def thermal_process_finished(self):
        """thermal_process_finished.

        :param self:
        """
        # switch between i8k and bios modes
        if self.p and self.p.exitCode() != 0:
            self.kill_process()
            self.thermal_checkbox.setChecked(False)
            self.bios_thermal_control_on = False
            self.supported_thermal_modes_data = []
            self.current_thermal_mode_data = []
            return
        else:
            self.close_process()
            self.update_thermal_modes()

    def thermal_process_disable_finished(self):
        """thermal_process_disable_finished.

        :param self:
        """
        if self.p and self.p.exitCode() != 0:
            self.kill_process()
            self.thermal_checkbox.setChecked(True)
            self.bios_thermal_control_on = True
            self.supported_thermal_modes_data = []
            self.current_thermal_mode_data = []
            return
        else:
            self.close_process()
            self.update_thermal_modes()

    def update_thermal_modes(self):
        """update_thermal_modes.

        :param self:
        """
        if self.supported_thermal_modes_data:
            self.supported_thermal_modes = self.sanitise_thermal_modes(
                self.supported_thermal_modes_data)
            # make dict with display and machine name variantions
            for mode in self.supported_thermal_modes:
                self.thermal_modes[mode] = mode.lower().replace(" ", "-")
            self.supported_thermal_modes_data = []

        if self.current_thermal_mode_data:
            self.current_thermal_mode_data = self.sanitise_thermal_modes(
                self.current_thermal_mode_data)
            self.current_thermal_mode = self.current_thermal_mode_data[
                0].title().replace("-", " ")
            self.current_thermal_mode_data = []

        # DEBUG
        # print(f"disable supported modes: {self.supported_thermal_modes}")
        # print(f"disable current mode {self.current_thermal_mode}")

        self.bios_thermal_control_on = bool(self.thermal_checkbox.isChecked(
        ) and self.current_thermal_mode is not None and self.is_smbios_active)
        self.switch_thermal_menu()

    def switch_thermal_menu(self):
        """switch_thermal_menu.

        :param self:
        """
        # remove and re-build thermal menu
        self.remove_thermal_menu()
        self.build_thermal_menu()

    def remove_thermal_menu(self):
        """remove_thermal_menu.

        :param self:
        """
        actions = self.thermal_menu_group.actions()
        for action in actions:
            self.thermal_control_menu.removeAction(action)
            self.thermal_menu_group.removeAction(action)
        self.thermal_menu_group.setEnabled(False)

    def build_thermal_menu(self):
        """build_thermal_menu.

        :param self:
        """
        if self.thermal_menu_group.actions():
            return

        if self.bios_thermal_control_on:
            if self.thermal_modes:
                for _name, _mach_name in self.thermal_modes.items():
                    thermal_menu_item = QAction(_name)
                    thermal_menu_item.setObjectName(_mach_name)
                    self._set_thermal_menu_actions(thermal_menu_item, True)
                self.thermal_menu_group.setEnabled(True)
                self.thermal_control_menu.setTitle("BIOS Thermal Management")
                actions = self.thermal_menu_group.actions()
                for action in actions:
                    if action.text() == self.current_thermal_mode:
                        action.setChecked(True)
        else:
            if self.is_i8kmon_active:
                self.i8k_info = self.get_i8k_information()
            if self.i8k_info:
                for key, value in self.i8k_info.items():
                    if key.startswith('Fan Mode'):
                        mode = key.split('Fan Mode ')
                        if ((self.left_fan_mode in self.left_fan_mode_map) and
                                (int(mode[1]) == self.left_fan_mode_map[self.left_fan_mode]) or
                                (self.right_fan_mode in self.right_fan_mode_map) and
                                (int(mode[1]) == self.right_fan_mode_map[self.right_fan_mode])):
                            high = "High Trigger: ".rjust(10)
                            if self.ac_power_supply_on:
                                low_data = f"{value[0]}{self.degree_sign}C\n".rjust(25)
                                high_data = f"{value[1]}{self.degree_sign}C".rjust(23)
                            else:
                                low_data = f"{value[2]}{self.degree_sign}C\n".rjust(25)
                                high_data = f"{value[3]}{self.degree_sign}C".rjust(23)
                            low = "Low Trigger: ".rjust(9)
                            title = (f"{key}\n{low}{low_data}{high}{high_data}")
                            self.fan_mode_menu_item.setText(title)
                            self.thermal_control_menu.addAction(
                                self.fan_mode_menu_item)
                            self.thermal_menu_group.addAction(
                                self.fan_mode_menu_item)
                    elif key.startswith('Left Speed'):
                        data = f"{value[value.index(str(self.left_fan_mode))]}".rjust(
                            15)
                        title = f"Max {key}: "
                        self.left_fan_thermal_menu_item.setText(title)
                        self.thermal_control_menu.addAction(
                            self.left_fan_thermal_menu_item)
                        self.thermal_menu_group.addAction(
                            self.left_fan_thermal_menu_item)
                    elif key.startswith('Right Speed'):
                        data = f"{value[value.index(str(self.right_fan_mode))]}".rjust(
                            15)
                        title = f"Max {key}: "
                        self.right_fan_thermal_menu_item.setText(title)
                        self.thermal_control_menu.addAction(
                            self.right_fan_thermal_menu_item)
                        self.thermal_menu_group.addAction(
                            self.right_fan_thermal_menu_item)
                    else:
                        data = f"{value}{self.degree_sign}C".rjust(16)
                        title = f"{key}: {data}"
                        thermal_menu_item = QAction(title)
                        self._set_thermal_menu_actions(thermal_menu_item, False)
                self.thermal_menu_group.setEnabled(True)
                self.thermal_control_menu.setTitle(
                    "I8K Thermal Management")
        if not self.is_smbios_active and not self.is_i8kmon_active:
            self.thermal_menu_group.setEnabled(False)
            self.thermal_control_menu.setTitle("Thermal Control N/A")

    def _set_thermal_menu_actions(self, thermal_menu_item, arg1):
        """_set_thermal_menu_actions.

        :param self:
        :param thermal_menu_item:
        :param arg1:
        """
        thermal_menu_item.setCheckable(arg1)
        self.thermal_control_menu.addAction(thermal_menu_item)
        self.thermal_menu_group.addAction(thermal_menu_item)

    def build_cpu_menu(self):
        """build_cpu_menu.

        :param self:
        """
        # build menu for per CPU core information
        if self.scaling_enabled:
            for i in range(self.cpu_num):
                self.cpu_curr_freqs.append(
                    f"/sys/devices/system/cpu/cpu{i}/cpufreq/scaling_cur_freq")
                c_action = QAction(f"CPU {i}")
                c_action.setCheckable(False)
                self.cpu_details.addAction(c_action)
                self.cpu_freq_list.append(c_action)
            # build menu for CPU turbo boost stats
            self.cpu_turbo.addAction(self.cpu_turbo_enabled)
            self.cpu_turbo.addAction(self.min_cpu_freq)
            self.cpu_turbo.addAction(self.max_cpu_freq)
            self.cpu_turbo.addAction(self.min_cpu_turbo)
            self.cpu_turbo.addAction(self.max_cpu_turbo)

    def get_cpu_tdp(self):
        """get_cpu_tdp.

        :param self:
        """
        if self.tdp_enabled:
            with open(self.tdp, 'r') as f:
                self.cpu_tdp.setText(f"{int(f.read().strip()) // 1000000} W")
            return self.cpu_tdp

    def get_i8k_information(self):
        """get_i8k_information.

        :param self:
        """
        if self.critical_error:
            return

        info = {}
        i8k_config = None
        try:
            i8k_config = open(self.i8kmon_conf, 'r')
        except IOError as e:
            self.critical_error = True
            if self.tray:
                self.tray.showMessage("i8kgui",
                                      f"Error cannot find i8kmon config file. "
                                      f"Please check your config file \n\n{e}",
                                      icon=QSystemTrayIcon.Critical,
                                      msecs=10000)
            else:
                print("Error: cannot find i8kmon config file")
        else:
            if i8k_config:
                for line in i8k_config:
                    if line.startswith('set config(t_high)'):
                        conf = line.split('set config(t_high)')[1].strip()
                        info['High CPU Temp'] = conf
                    if line.startswith('set config(0)'):
                        split_line = line.split('{')
                        conf = split_line[2].split('}')
                        info['Fan Mode 0'] = list(conf[1].split())
                    if line.startswith('set config(1)'):
                        split_line = line.split('{')
                        conf = split_line[2].split('}')
                        info['Fan Mode 1'] = list(conf[1].split())
                    if line.startswith('set config(2)'):
                        split_line = line.split('{')
                        conf = split_line[2].split('}')
                        info['Fan Mode 2'] = list(conf[1].split())
                    if line.startswith('set config(3)'):
                        split_line = line.split('{')
                        conf = split_line[2].split('}')
                        info['Fan Mode 3'] = list(conf[1].split())
                    if line.startswith('set status(leftspeed)'):
                        conf = line.split(
                            "set status(leftspeed)")[1].strip().strip('"')
                        info['Left Speed'] = list(conf.split())
                    if line.startswith('set status(rightspeed)'):
                        conf = line.split(
                            "set status(rightspeed)")[1].strip().strip('"')
                        info['Right Speed'] = list(conf.split())
                i8k_config.close()
        # set status(leftspeed/rightspeed) must be set in i8kmon.conf
        if 'Left Speed' in info:
            for i in range(len(info['Left Speed'])):
                self.left_fan_mode_map[int(info['Left Speed'][i])] = i
        if 'Right Speed' in info:
            for i in range(len(info['Right Speed'])):
                self.right_fan_mode_map[int(info['Right Speed'][i])] = i
        return info

    def get_smbios_information(self):
        """get_smbios_information.

        :param self:
        """
        if self.p_smbios is not None:
            return

        # find smbios script
        if self.is_smbios_active:
            smbios_file = Path(str(shutil.which("smbios-sys-info")))
            if not smbios_file.is_file():
                if self.tray:
                    self.tray.showMessage(
                        "i8kgui",
                        "Error please install python3-libsmbios, "
                        "smbios-sys-info not found",
                        icon=QSystemTrayIcon.Critical,
                        msecs=10000)
                return
            # Keep a reference to the QProcess (e.g. on self) while it's running.
            self.p_smbios = QProcess()
            self.p_smbios.readyReadStandardOutput.connect(self.handle_smbios_stdout, type=Qt.QueuedConnection)
            self.p_smbios.readyReadStandardError.connect(self.handle_smbios_stderr, type=Qt.QueuedConnection)
            self.p_smbios.stateChanged.connect(self.smbios_handle_process_state, type=Qt.QueuedConnection)
            # Clean up once complete.
            self.p_smbios.finished.connect(self.smbios_process_finished, type=Qt.QueuedConnection)
            self.p_smbios.start("pkexec", [str(smbios_file.resolve())])
        else:
            print("Error: libsmbios is unavailable")

    def handle_smbios_stderr(self):
        """handle_smbios_stderr.

        :param self:
        """
        if self.p_smbios:
            data = self.p_smbios.readAllStandardError()
            stderr = bytes(data).decode("utf8")
            if 'Request dismissed' in stderr:
                self.p_smbios.finished.emit(1, QProcess.CrashExit)
            else:
                print(f"Error: handle_smbios_stderr {stderr}")

    def handle_smbios_stdout(self):
        """handle_smbios_stdout.

        :param self:
        """
        if self.p_smbios:
            data = self.p_smbios.readAllStandardOutput()
            stdout = bytes(data).decode("utf8")
            self.smbios_message(stdout)

    def handle_process_state(self, state):
        """handle_process_state.

        :param state:
        """
        states = {
            QProcess.NotRunning: 'NotRunning',
            QProcess.Starting: 'Starting',
            QProcess.Running: 'Running',
        }
        self.p_state = states[state]

    def smbios_handle_process_state(self, state):
        """handle_process_state.

        :param state:
        """
        states = {
            QProcess.NotRunning: 'NotRunning',
            QProcess.Starting: 'Starting',
            QProcess.Running: 'Running',
        }
        self.p_state_smbios = states[state]

    def smbios_process_finished(self):
        """smbios_process_finished.

        :param self:
        """
        if self.p_smbios and self.p_smbios.exitCode() != 0:
            self.kill_process(self.p_smbios)
        else:
            self.close_process(self.p_smbios)
            self.process_smbios_data()

    def process_smbios_data(self):
        """process_smbios_data.

        :param self:
        """
        if self.smbios_data:
            self.smbios_data.pop()  # remove white space
            self.smbios_info = {
                self.smbios_data[i]: self.smbios_data[i + 1]
                for i in range(0, len(self.smbios_data), 2)
            }  # convert data to dict
            self.smbios_data = []  # empty data container
            f_layout = self.info_dialog.layout()
            for key, value in self.smbios_info.items():
                f_layout.insertRow(f_layout.rowCount() - 2, str(key),
                                   QLabel(str(value)))
            self.smbios_button.setDisabled(True)

    def create_info_dialog(self):
        """create_info_dialog.

        :param self:
        """
        close_button = QPushButton("close", self.info_dialog)
        close_button.setMaximumWidth(100)
        close_button.clicked.connect(self.close_info_dialog,
                                     type=Qt.QueuedConnection)

        self.smbios_button.setMaximumWidth(250)
        self.smbios_button.clicked.connect(self.get_smbios_information,
                                           type=Qt.QueuedConnection)

        f_layout = QFormLayout()
        f_layout.addRow("CPU Model", self.cpu_model)
        f_layout.addRow("CPU TDP", self.get_cpu_tdp())
        f_layout.addRow("i8k Format Version", self.i8k_format_version)
        f_layout.addRow("BIOS Version", self.bios_version)
        f_layout.addRow("Service Tag", self.service_tag)
        f_layout.addRow("A/C Power Supply", self.power_status)
        f_layout.addRow("Button Status", self.button_status)
        f_layout.addRow(" ", None)
        f_layout.addRow("(requires password)", self.smbios_button)
        f_layout.addRow(" ", None)
        f_layout.addRow(" ", None)
        f_layout.addWidget(close_button)
        if self.smbios_info:
            for key, value in self.smbios_info.items():
                f_layout.insertRow(f_layout.rowCount() - 2, str(key),
                                   QLabel(str(value)))
        self.info_dialog.setLayout(f_layout)
        # set dialog geometry
        self.info_dialog.setWindowTitle("Information")
        self.info_dialog.setModal(False)
        self.info_dialog.adjustSize()

    def create_settings_dialog(self):
        """create_settings_dialog.

        :param self:
        """
        self.save_button.setMaximumWidth(100)

        self.always_on_checkbox.setText("Collect statistics continuously")
        self.always_on_checkbox.stateChanged.connect(self.always_collect,
                                                     type=Qt.QueuedConnection)

        self.thermal_checkbox.setText("Enable BIOS thermal management")
        self.thermal_checkbox.stateChanged.connect(self.handle_thermal_control, type=Qt.QueuedConnection)

        self.cpu_ave_freq_checkbox.setText("Display average CPU freq.")
        self.cpu_ave_freq_checkbox.stateChanged.connect(
            self.enable_ave_cpu_freq, type=Qt.QueuedConnection)

        self.interval_input.setValidator(QIntValidator())
        self.interval_input.setMaxLength(6)
        self.interval_input.setText(str(self.monitor_interval))

        self.save_button.clicked.connect(self.save_dialog_settings, type=Qt.QueuedConnection)
        f_layout = QFormLayout()
        f_layout.addRow("(requires password)", self.thermal_checkbox)
        f_layout.addRow(" ", self.always_on_checkbox)
        f_layout.addRow(" ", self.cpu_ave_freq_checkbox)
        f_layout.addRow(" ", None)
        f_layout.addRow("Monitoring Interval (milliseconds):",
                        self.interval_input)
        f_layout.addRow(" ", None)
        f_layout.addRow(self.save_button)
        self.settings_dialog.setLayout(f_layout)
        # set dialog geometry
        self.settings_dialog.setWindowTitle("Settings")
        self.settings_dialog.setModal(False)
        self.settings_dialog.adjustSize()

    def load_sys_tray(self):
        """load_sys_tray.

        :param self:
        """
        # build desktop system tray
        self.tray = QSystemTrayIcon(self)
        if self.tray.isSystemTrayAvailable():
            # check system then user icon locations
            icon_file = Path("/usr/share/icons/i8kgui_icon.png").resolve()
            if not icon_file.is_file():
                icon_file = Path(f'{str(Path.home())}/.local/share/icons/i8kgui_icon.png').resolve()
                if not icon_file.is_file():
                    pass
            icon = QIcon(str(icon_file))
            if icon.pixmap(QSize(64, 64)).isNull():
                icon = QIcon('icons/i8kgui_icon.png')
            self.tray.setIcon(icon)
            self.tray.activated.connect(self.status, type=Qt.QueuedConnection)
            self.tray.messageClicked.connect(self.message_clicked, type=Qt.QueuedConnection)
            # workaround because tray activated only works with double click
            self.tray_menu.aboutToShow.connect(self.monitor, type=Qt.QueuedConnection)
            self.tray_menu.aboutToHide.connect(self.stop_monitor, type=Qt.QueuedConnection)
            # setup display
            self.tray_menu.addAction(self.cpu_usage)
            self.tray_menu.addAction(self.cpu_temp)
            self.tray_menu.addAction(self.cpu_mhz)
            self.tray_menu.addAction(self.cpu_governor)
            self.connect_cpupower()
            self.tray_menu.addMenu(self.cpu_details)
            self.tray_menu.addMenu(self.cpu_turbo)
            self.tray_menu.addSeparator()
            self.tray_menu.addAction(self.left_fan_rpm)
            self.tray_menu.addAction(self.right_fan_rpm)
            self.tray_menu.addAction(self.left_fan_status)
            self.tray_menu.addAction(self.right_fan_status)
            self.tray_menu.addSeparator()
            # setup thermal control management menu
            self.tray_menu.addMenu(self.thermal_control_menu)
            self.thermal_menu_group.setExclusive(True)
            self.thermal_menu_group.triggered.connect(
                self.thermal_mode_selection, type=Qt.QueuedConnection)
            self.build_thermal_menu()  # menu created based on settings
            self.tray_menu.addSeparator()
            # add info, settings and quit menu items
            action_info = self.tray_menu.addAction("Information")
            action_info.triggered.connect(self.show_info, type=Qt.QueuedConnection)
            action_settings = self.tray_menu.addAction("Settings")
            action_settings.triggered.connect(self.show_settings, type=Qt.QueuedConnection)
            action_quit = self.tray_menu.addAction("Quit")
            action_quit.triggered.connect(self.close_app, type=Qt.QueuedConnection)

            self.tray.setContextMenu(self.tray_menu)
            self.tray.setToolTip("i8kgui")
            self.tray.setVisible(True)
        else:
            QMessageBox.critical(
                None, "i8kgui", "Unable to locate system tray on this system.")
            self.close_app()

    def connect_cpupower(self):
        """connect_cpupower.

        :param self:
        """
        # connect to cpupower-gui if found
        cpupower_file = Path(str(shutil.which("cpupower-gui")))
        if cpupower_file.is_file():
            self.cpu_governor.triggered.connect(self.show_cpupower, type=Qt.QueuedConnection)
        # TODO - if not found show message to user to install

    def show_cpupower(self):
        """show_cpupower.

        :param self:
        """
        if self.cpupower is None:
            self.cpupower = QProcess()
            self.cpupower.finished.connect(self.show_cpupower_finished, type=Qt.QueuedConnection)
            self.cpupower.errorOccurred.connect(self.show_cpupower_error, type=Qt.QueuedConnection)
            self.cpupower.start(str(shutil.which("cpupower-gui")))
            self.cpupower.waitForFinished(1000)

    def show_cpupower_finished(self):
        if self.cpupower:
            print("Info: cpupower-gui finished")
            if self.cpupower.state() != 0:
                self.kill_process(self.cpupower)
            self.cpupower = None

    def show_cpupower_error(self):
        if self.cpupower:
            print("Error: cpupower-gui failed")
            if self.cpupower.state() != 0:
                self.kill_process(self.cpupower)
            self.cpupower = None

    def thermal_mode_selection(self, selected_mode):
        """thermal_mode_selection. The purpose of this function is to switch the BIOS thermal mode based on the user's
        input. If the `smbios-thermal-ctl` script is not found, the function prints an error and returns.

        :param selected_mode: the BIOS thermal mode selected
        """
        if self.bios_thermal_control_on:
            if self.p is None:
                # find libsmbios script
                thermal_file = Path(str(shutil.which("smbios-thermal-ctl")))
                if not thermal_file.is_file():
                    if self.tray:
                        self.tray.showMessage(
                            "i8kgui",
                            "Error please install python3-libsmbios, "
                            "smbios-thermal-ctl not found.",
                            icon=QSystemTrayIcon.Critical,
                            msecs=10000)
                    return
                # switch BIOS thermal mode (i.e. performance, balanced, etc)
                if self.current_thermal_mode != selected_mode.text():
                    self._set_thermal_mode_selection(selected_mode, thermal_file)
        else:
            print("Edit i8kmon config")  # TODO - add i8kmon config updating

    def _set_thermal_mode_selection(self, selected_mode, smbios_ctl_file):
        """_set_thermal_mode_selection. The purpose of this function is to set the thermal mode by executing `pkexec`
        with the appropriate arguments. It uses a QProcess to handle the execution of the `pkexec` command
        and connect the necessary signals to handle the output, errors and state changes.

        :param self:
        :param selected_mode: the BIOS thermal mode selected
        :param smbios_ctl_file:
        """
        t_args = "--set-thermal-mode="
        t_args += (
            self.thermal_modes[selected_mode.text()]
            if selected_mode.text() in self.thermal_modes
            else 'balanced'
        )
        # Keep a reference to the QProcess (e.g. on self)
        # while it's running.
        self.p = QProcess()
        self.p.readyReadStandardOutput.connect(
            self.handle_thermal_stdout, type=Qt.QueuedConnection)
        self.p.readyReadStandardError.connect(
            self.handle_thermal_stderr, type=Qt.QueuedConnection)
        self.p.stateChanged.connect(self.handle_process_state, type=Qt.QueuedConnection)
        # Clean up once complete.
        self.p.finished.connect(self.thermal_selection_finished, type=Qt.QueuedConnection)
        self.p.start("pkexec", [str(smbios_ctl_file.resolve()), t_args])

    @Slot()
    def message_clicked(self):
        """message_clicked.

        :param self:
        """
        self.close_app()

    def read_i8kmon_info(self):
        """read_i8kmon_info.

        :param self:
        """
        with self.open_sysfs_file(self.i8kmon_fan1) as (fan1), \
                self.open_sysfs_file(self.i8kmon_fan2) as (fan2), \
                self.open_sysfs_file(self.i8kmon_fan1_target) as (fan1_target), \
                self.open_sysfs_file(self.i8kmon_fan2_target) as (fan2_target), \
                self.open_sysfs_file(self.i8kmon_temp) as (temp), \
                self.open_sysfs_file(self.cpu_no_turbo) as (no_turbo), \
                self.open_sysfs_file(self.cpu_min_perf) as (min_perf), \
                self.open_sysfs_file(self.cpu_max_perf) as (max_perf), \
                self.open_sysfs_file(self.cpu_min_scaling) as (min_scaling), \
                self.open_sysfs_file(self.cpu_max_scaling) as (max_scaling):
            i8kmon_fan1 = fan1.readline().strip('\n') if fan1 else "0"
            i8kmon_fan2 = fan2.readline().strip('\n') if fan2 else "0"
            i8kmon_fan1_target = fan1_target.readline().strip('\n') if fan1_target else "0"
            i8kmon_fan2_target = fan2_target.readline().strip('\n') if fan2_target else "0"
            i8kmon_temp = temp.readline().strip('\n') if temp else "0"
            cpu_no_turbo = no_turbo.readline().strip('\n') if no_turbo else "0"
            cpu_min_perf = min_perf.readline().strip('\n') if min_perf else "N/A"
            cpu_max_perf = max_perf.readline().strip('\n') if max_perf else "N/A"
            cpu_min_scaling = min_scaling.readline().strip('\n') if min_scaling else "0"
            cpu_max_scaling = max_scaling.readline().strip('\n') if max_scaling else "0"
            # cpu temp
            c_temp = f"{float(i8kmon_temp.strip()) / 1000}{self.degree_sign}C".rjust(20)
            self.cpu_temp.setText(f"CPU Temp:\t{c_temp}")
            # left fan speed
            l_fan = f"{i8kmon_fan1.strip()}  RPM".rjust(22)
            self.left_fan_rpm.setText(f"Left Fan:\t{l_fan}")
            # right fan speed
            r_fan = f"{i8kmon_fan2.strip()}  RPM".rjust(22)
            self.right_fan_rpm.setText(f"Right Fan:\t{r_fan}")
            # left fan mode
            l_mode = int(i8kmon_fan1_target.strip())
            if l_mode in self.left_fan_mode_map:
                left_mode = f"{self.left_fan_mode_map[l_mode]}".rjust(
                    15)
            else:
                left_mode = "N/A".rjust(15)
            self.left_fan_status.setText(f"Left Fan Mode:{left_mode}")
            # right fan mode
            r_mode = int(i8kmon_fan2_target.strip())
            if r_mode in self.right_fan_mode_map:
                right_mode = f"{self.right_fan_mode_map[r_mode]}".rjust(
                    13)
            else:
                right_mode = "N/A".rjust(13)
            self.right_fan_status.setText(f"Right Fan Mode:{right_mode}")
            # cpu turbo
            if self.is_intel_cpu and no_turbo:
                t_status = 'Enabled' if int(cpu_no_turbo.strip()) == 0 else 'Disabled'
            elif not self.is_intel_cpu and no_turbo:
                t_status = 'Disabled' if int(cpu_no_turbo.strip()) == 0 else 'Enabled'
            else:
                t_status = 'N/A'
            self.cpu_turbo_enabled.setText(f"CPU Turbo Boost:\t{t_status}")
            c_min_freq = int(cpu_min_scaling.strip()) / 1000
            c_min_freq = f"{c_min_freq:.0f} MHz".rjust(17)
            self.min_cpu_freq.setText(f"CPU Min. Freq:\t{c_min_freq}")
            c_max_freq = int(cpu_max_scaling.strip()) / 1000
            c_max_freq = f"{c_max_freq:.0f} MHz".rjust(17)
            self.max_cpu_freq.setText(f"CPU Max. Freq:\t{c_max_freq}")
            c_min_turbo = cpu_min_perf.strip()
            c_min_turbo = f"{c_min_turbo} %"
            self.min_cpu_turbo.setText(f"CPU Min. Turbo:\t{c_min_turbo}")
            c_max_turbo = cpu_max_perf.strip()
            c_max_turbo = f"{c_max_turbo} %"
            self.max_cpu_turbo.setText(f"CPU Max. Turbo:\t{c_max_turbo}")
            # update fan modes
            if self.left_fan_mode != int(
                    i8kmon_fan1_target.strip()) or self.right_fan_mode != int(
                        i8kmon_fan2_target.strip()):
                self.left_fan_mode = int(i8kmon_fan1_target.strip())
                self.right_fan_mode = int(i8kmon_fan2_target.strip())
                self.update_i8k_thermal()
            # update power status
            self.update_i8k_ac_power()
            self.power_status.setText(str(self.ac_power_supply_on))

    def read_cpu_info(self):
        """read_cpu_info.

        :param self:
        """
        # gather current CPU MHz.
        try:
            all_mhz = []
            if self.scaling_enabled:
                cpu_procs = []
                for cpu in self.cpu_curr_freqs:
                    with open(cpu, 'r') as proc:
                        cpu_procs.append(proc)
                        for temp in proc:
                            mhz = int(temp.strip('\n')) // 1000
                            all_mhz.append(mhz)
            coretemps = []
            for coretemp in self.coretemps_sysfs:
                with open(coretemp, 'r') as cpu_temps:
                    coretemps.extend(
                        float(temp.strip('\n')) / 1000 for temp in cpu_temps)
        except IOError as e:
            self.critical_error = True
            if self.tray:
                self.tray.showMessage(
                    "i8kgui",
                    f"Error cannot find /proc/cpuinfo.\n\n{e}",
                    icon=QSystemTrayIcon.Critical,
                    msecs=10000,
                )
        else:
            self._get_cpu_temps_load_freqs(all_mhz, coretemps)

    def _get_cpu_temps_load_freqs(self, all_mhz, coretemps):
        """_get_cpu_temps_load_freqs.

        :param self:
        :param all_mhz:
        :param coretemps:
        """
        usage = psutil.cpu_percent(interval=0.0, percpu=False)
        usage_per = psutil.cpu_percent(interval=0.0, percpu=True)
        c_use = f"{usage} %".rjust(20)
        self.cpu_usage.setText(f"CPU Usage:\t{c_use}")
        if self.use_cpu_ave_freq and all_mhz:
            total_mhz = sum(all_mhz)
            mhz = total_mhz // len(all_mhz)
        elif all_mhz:
            all_mhz.sort()
            mhz = all_mhz[-1]
        else:
            mhz = "N/A"
        c_mhz = f"{mhz} MHz".rjust(20)
        self.cpu_mhz.setText(f"CPU Freq:\t{c_mhz}")
        for i, c in enumerate(self.cpu_freq_list):
            if self.cpu_temp_sensors and len(self.cpu_temp_sensors) > 1:
                j = self.cpu_temp_sensors[i]
            elif self.cpu_temp_sensors:
                j = self.cpu_temp_sensors[0]
            else:
                j = 0
            # CPU number - 7 chars "CPU 01:"
            _num = "{:02d}".format(i)
            cpu_text = f"CPU {_num}:"
            # CPU usage - 7 chars "100.0 %"
            if usage_per and len(usage_per) > 1:
                usage = str(usage_per[i]).center(5)
            elif usage_per:
                usage = str(usage_per[0]).center(5)
            else:
                usage = str(0.0).center(5)
            usage_text = f"{usage} %"
            # CPU MHz - 8 chars "4000 MHz"
            mhz_text = f"{all_mhz[i]:04d} MHz" if all_mhz else "N/A MHz"
            # CPU core Temp - 7 chars "100 C"
            if coretemps and len(coretemps) > 1:
                temp = "{:2.0f}".format(coretemps[j]).rjust(4)
            elif coretemps:
                temp = "{:2.0f}".format(coretemps[0]).rjust(4)
            else:
                temp = "{:2.0f}".format(0.0).rjust(4)
            temp_text = f"{temp}{self.degree_sign}C"
            # Output "CPU 01: 100.0 % 4000 MHz 80 C" - total 29 chars
            per_cpu_text = cpu_text.rjust(7) + usage_text.center(
                10) + mhz_text.center(9) + temp_text.ljust(7)
            c.setText(f"{per_cpu_text}".center(36))

    def read_cpu_gov_info(self):
        """read_cpu_gov_info. Gather current CPU governor

        :param self:
        """
        if self.scaling_enabled:
            try:
                with open(self.scaling_sysfs, 'r') as gov_sys:
                    gov = gov_sys.readline().strip()
                    c_gov = f"{gov.capitalize()}".rjust(15)
                    self.cpu_governor.setText(f"CPU Governor: {c_gov}")
            except IOError as e:
                self.critical_error = True
                if self.tray:
                    self.tray.showMessage(
                        "i8kgui", f"Error cannot find "
                        f"/sys/devices/system/cpu/cpu0/cpufreq/scaling_governor."
                        f"\n\n{e}",
                        icon=QSystemTrayIcon.Critical,
                        msecs=10000)

    def read_power_supply_status(self):
        """read_power_supply_status.

        :param self:
        """
        # gather power supply status
        if self.power_enabled:
            with open(self.power_sysfs, 'r') as pwr_sys:
                pwr_ac = pwr_sys.readline().strip()
                self.ac_power_supply_on = int(pwr_ac) == 1

    def read_proc(self):
        """read_proc.

        :param self:
        """
        # read i8k, CPU and power values then update the system tray
        # and send a fake click to initiate live updating while
        # the system tray is in active use.
        if self.critical_error:
            return
        self.read_power_supply_status()
        self.read_i8kmon_info()
        self.read_cpu_info()
        self.read_cpu_gov_info()
        # send fake single-click event as workaround
        if self.tray:
            self.tray.activated.emit(QSystemTrayIcon.ActivationReason.Trigger)

    def update_i8k_ac_power(self):
        """update_i8k_ac_power.

        :param self:
        """
        # update i8k thermal menu fan mode based on A/C power supply status
        if not self.i8k_info:
            return

        for key, value in self.i8k_info.items():
            if key.startswith('Fan Mode'):
                mode = key.split('Fan Mode ')
                if ((self.left_fan_mode in self.left_fan_mode_map) and
                        (int(mode[1]) == self.left_fan_mode_map[self.left_fan_mode]) or
                        (self.right_fan_mode in self.right_fan_mode_map) and
                        (int(mode[1]) == self.right_fan_mode_map[self.right_fan_mode])):
                    high = "High Trigger: ".rjust(10)
                    if self.ac_power_supply_on:
                        low_data = f"{value[0]}{self.degree_sign}C\n".rjust(25)
                        high_data = f"{value[1]}{self.degree_sign}C".rjust(23)
                    else:
                        low_data = f"{value[2]}{self.degree_sign}C\n".rjust(25)
                        high_data = f"{value[3]}{self.degree_sign}C".rjust(23)
                    low = "Low Trigger: ".rjust(9)
                    title = (f"{key}\n{low}{low_data}{high}{high_data}")
                    self.fan_mode_menu_item.setText(title)
                    break

    def update_i8k_thermal(self):
        """update_i8k_thermal.

        :param self:
        """
        # update i8k thermal menu. Activated when i8k changes fan mode.
        if not self.i8k_info:
            if self.is_i8kmon_active:
                self.i8k_info = self.get_i8k_information()
        if self.i8k_info:
            for key, value in self.i8k_info.items():
                if key.startswith('Fan Mode'):
                    mode = key.split('Fan Mode ')
                    if ((self.left_fan_mode in self.left_fan_mode_map) and
                            (int(mode[1]) == self.left_fan_mode_map[self.left_fan_mode]) or
                            (self.right_fan_mode in self.right_fan_mode_map) and
                            (int(mode[1]) == self.right_fan_mode_map[self.right_fan_mode])):
                        high = "High Trigger: ".rjust(10)
                        if self.ac_power_supply_on:
                            low_data = f"{value[0]}{self.degree_sign}C\n".rjust(25)
                            high_data = f"{value[1]}{self.degree_sign}C".rjust(23)
                        else:
                            low_data = f"{value[2]}{self.degree_sign}C\n".rjust(25)
                            high_data = f"{value[3]}{self.degree_sign}C".rjust(23)
                        low = "Low Trigger: ".rjust(9)
                        title = (f"{key}:\n{low}{low_data}{high}{high_data}")
                        self.fan_mode_menu_item.setText(title)
                elif key.startswith('Left Speed'):
                    left_speed = f"{value[value.index(str(self.left_fan_mode))]}".rjust(
                        15)
                    title = f"Max {key}: "
                    self.left_fan_thermal_menu_item.setText(
                        f"{title}{left_speed}")
                elif key.startswith('Right Speed'):
                    right_speed = f"{value[value.index(str(self.right_fan_mode))]}".rjust(
                        12)
                    title = f"Max {key}: "
                    self.right_fan_thermal_menu_item.setText(
                        f"{title}{right_speed}")

    def status(self, reason):
        """status.

        :param reason:
        """
        # start live updating of values if system tray is in active use,
        # and employ workaround for timer being started upon load.
        if reason == QSystemTrayIcon.ActivationReason.Trigger:
            if not self.monitorTimer.isActive():
                if not self.first_load:
                    self.monitorTimer.start()
                else:
                    # workaround to not start QTimer
                    # when tray is loaded first time
                    self.first_load = False

    def monitor(self):
        """monitor.

        :param self:
        """
        self.read_proc()

    def refresh_monitor(self):
        """refresh_monitor.

        :param self:
        """
        self.read_proc()

    def stop_monitor(self):
        """stop_monitor.

        :param self:
        """
        if not self.always_on and self.monitorTimer.isActive():
            self.monitorTimer.stop()

    def show_settings(self):
        """show_settings.

        :param self:
        """
        center_point = QScreen.availableGeometry(
            QApplication.primaryScreen()).center()
        fg = self.settings_dialog.frameGeometry()
        fg.moveCenter(center_point)
        self.settings_dialog.move(fg.topLeft())
        if not self.settings_dialog.isVisible():
            self.settings_dialog.show()
        self.settings_dialog.raise_()
        self.settings_dialog.setFocus()
        pointer = self.cursor()
        pointer.setPos(center_point)

    def show_info(self):
        """show_info.

        :param self:
        """
        if not self.is_i8kmon_active:
            return
        if self.i8kmon_ctl:
            with os.popen(self.i8kmon_ctl) as ctl:
                self._get_i8kctl_info(ctl)
        center_point = QScreen.availableGeometry(
            QApplication.primaryScreen()).center()
        fg = self.info_dialog.frameGeometry()
        fg.moveCenter(center_point)
        self.info_dialog.move(fg.topLeft())
        if not self.info_dialog.isVisible():
            self.info_dialog.show()
        self.info_dialog.raise_()
        self.info_dialog.setFocus()
        pointer = self.cursor()
        pointer.setPos(center_point)

    def _get_i8kctl_info(self, ctl):
        """_get_i8kctl_info.

        :param self:
        :param ctl:
        """
        line = ctl.readline().strip('\n').strip()
        split_line = line.split(" ")
        self.i8k_format_version.setText(split_line[0].strip())
        self.bios_version.setText(split_line[1].strip())
        self.service_tag.setText(split_line[2].strip())
        self.button_status.setText(split_line[9].strip())

    @contextmanager
    def open_sysfs_file(self, f_name, mode="r"):
        """open_sysfs_file. A context manager for 'with' for opening a possible missing file in Sysfs.

        :param self:
        :param f_name:
        :param mode:
        """
        try:
            f = open(f_name, mode)
        except IOError as err:
            print(f"Warn: Sysfs IOError {err}")
            yield None
        else:
            try:
                yield f
            finally:
                f.close()


if __name__ == '__main__':
    app = QApplication([])
    app.setOrganizationName("i8kgui")
    app.setApplicationName("i8kgui")
    app.setApplicationVersion("0.8.4")
    # setup Dark Mode QPalette - initial setup
    palette = QPalette()
    palette.setColor(QPalette.Window, QColor(53, 53, 53))
    palette.setColor(QPalette.WindowText, Qt.white)
    palette.setColor(QPalette.Light, QColor(68, 68, 68))
    palette.setColor(QPalette.Base, QColor(25, 25, 25))
    palette.setColor(QPalette.AlternateBase, QColor(53, 53, 53))
    palette.setColor(QPalette.ToolTipBase, Qt.white)
    palette.setColor(QPalette.ToolTipText, Qt.black)
    palette.setColor(QPalette.Text, Qt.white)
    palette.setColor(QPalette.Button, QColor(53, 53, 53))
    palette.setColor(QPalette.ButtonText, Qt.white)
    palette.setColor(QPalette.BrightText, Qt.red)
    palette.setColor(QPalette.Link, QColor(42, 130, 218))
    palette.setColor(QPalette.Highlight, QColor(42, 130, 218, 192))
    palette.setColor(QPalette.HighlightedText, Qt.white)
    # setup Dark Mode disabled QPalette
    palette.setColor(QPalette.Disabled, QPalette.Window, Qt.black)
    palette.setColor(QPalette.Disabled, QPalette.WindowText,
                     QColor(255, 255, 255, 128))
    palette.setColor(QPalette.Disabled, QPalette.Base, QColor(68, 68, 68))
    palette.setColor(QPalette.Disabled, QPalette.Text,
                     QColor(255, 255, 255, 128))
    palette.setColor(QPalette.Disabled, QPalette.Button,
                     QColor(53, 53, 53, 128))
    palette.setColor(QPalette.Disabled, QPalette.ButtonText,
                     QColor(255, 255, 255, 128))
    palette.setColor(QPalette.Disabled, QPalette.BrightText, Qt.black)
    palette.setColor(QPalette.Disabled, QPalette.Link, Qt.black)
    palette.setColor(QPalette.Disabled, QPalette.Highlight, Qt.black)
    palette.setColor(QPalette.Disabled, QPalette.HighlightedText, Qt.black)
    # add Dark Mode to i8kgui
    app.setPalette(palette)

    app.setQuitOnLastWindowClosed(False)
    tray = I8kGui()

    app.exec_()
