#!/usr/bin/python3.14
# -*- coding: utf-8 -*-

# 2009-12-27 David Goncalves - Version 1.2
#            Total rewrite of NUT-Monitor to optimize GUI interaction.
#            Added favorites support (saved to user's home)
#            Added status icon on the notification area
#
# 2010-02-26 David Goncalves
#            Added UPS vars display and the possibility to change values
#            when user double-clicks on a RW var.
#
# 2010-05-01 David Goncalves
#            Added support for PyNotify (if available)
#
# 2010-05-05 David Goncalves
#            Added support for command line options
#            -> --start-hidden
#            -> --favorite
#
#            NUT-Monitor now tries to detect if there is a NUT server
#            on localhost and if there is 1 UPS, connects to it.
#
# 2010-10-06 David Goncalves - Version 1.3
#            Added localisation support
#
# 2015-02-14 Michal Fincham - Version 1.3.1
#            Corrected unsafe permissions on ~/.nut-monitor (Debian #777706)
#
# 2022-02-20 Luke Dashjr - Version 2.0
#            Port to Python 3 with PyQt5.
#
# 2023-11-27 Laurent Bigonville - Version 2.0.1
#            Set the DesktopFileName
#
# 2025-03-15 Jim Klimov - Version 2.0.2
#            Revise localisation, inject PACKAGE_VERSION and NUT_WEBSITE_BASE
#            into the About dialog contents
#
# 2025-05-08 Sebastian Kuttnig - Version 2.0.3
#            Port to Python 3 with PyQt6.
#

import PyQt6.uic
from PyQt6.QtCore import *
from PyQt6.QtGui import *
from PyQt6.QtWidgets import *

import sys
import base64
import os, os.path
import stat
import platform
import time
import optparse
import configparser
import locale
import gettext

# Try local development/accompanying packaging first,
# then system installation
sys_path_orig = sys.path.copy()
try:
    # Try "module" path relative to this program script:
    mod_path = os.path.sep.join([
        os.path.dirname(os.path.abspath(os.path.realpath(__file__))),
        "..", "module"])
    ### sys.stderr.write("[D] mod_path: %s\n" % mod_path)
    if os.path.isfile(os.path.sep.join([mod_path, "PyNUT.py"])):
        # Insert into the system path at index 1 to ensure that it
        # resolves after the main script but before anything else:
        sys.path.insert(1, mod_path)
        ### sys.stderr.write("[D] sys.path: %s\n" % sys.path)

    # Try our chances with the default or augmented path
    import PyNUT
except Exception as ignored:
    # ModuleNotFoundError, path/parsing, et al
    # Try our chances with the default path:
    sys.path = sys_path_orig
    ### sys.stderr.write("[D2] sys.path: %s\n" % sys.path)
    import PyNUT

# We would seek locale files relative to script dir
os.chdir(os.path.dirname(os.path.abspath(os.path.realpath(__file__))))
# print(os.getcwd())

class interface :

    DESIRED_FAVORITES_DIRECTORY_MODE = 0o700

    __widgets                        = {}
    __callbacks                      = {}
    __favorites                      = {}
    __favorites_file                 = None
    __favorites_path                 = ""
    __fav_menu_items                 = list()
    __ui_file                        = None
    __connected                      = False
    __online                         = None
    __ups_handler                    = None
    __ups_commands                   = None
    __ups_vars                       = None
    __ups_rw_vars                    = None
    __gui_updater                    = None
    __current_ups                    = None
    __quitting                       = False

    def __init__( self, argv ) :

        # Before anything, parse command line options if any present...
        opt_parser = optparse.OptionParser()
        opt_parser.add_option( "-H", "--start-hidden", action="store_true", default=False, dest="hidden", help="Start iconified in tray" )
        opt_parser.add_option( "-F", "--favorite", dest="favorite", help="Load the specified favorite and connect to UPS" )

        ( cmd_opts, args ) = opt_parser.parse_args()


        self.__app = QApplication( argv )
        try:
            self.__app.setDesktopFileName("nut-monitor-py3qt5")
        except Exception as ex:
            pass

        self.__ui_file = self.__find_res_file( 'ui', "window1.ui" )

        self.__widgets["interface"]                   = PyQt6.uic.loadUi( self.__ui_file )
        self.__widgets["main_window"]                 = self.__widgets["interface"]
        self.__widgets["status_bar"]                  = self.__widgets["interface"].statusbar2
        self.__widgets["ups_host_entry"]              = self.__widgets["interface"].entry1
        self.__widgets["ups_port_entry"]              = self.__widgets["interface"].spinbutton1
        self.__widgets["ups_refresh_button"]          = self.__widgets["interface"].button1
        self.__widgets["ups_authentication_check"]    = self.__widgets["interface"].checkbutton1
        self.__widgets["ups_authentication_frame"]    = self.__widgets["interface"].hbox1
        self.__widgets["ups_authentication_login"]    = self.__widgets["interface"].entry2
        self.__widgets["ups_authentication_password"] = self.__widgets["interface"].entry3
        self.__widgets["ups_list_combo"]              = self.__widgets["interface"].combobox1
        self.__widgets["ups_commands_combo"]          = self.__widgets["interface"].ups_commands_combo
        self.__widgets["ups_commands_button"]         = self.__widgets["interface"].button8
        self.__widgets["ups_connect"]                 = self.__widgets["interface"].button2
        self.__widgets["ups_disconnect"]              = self.__widgets["interface"].button7
        self.__widgets["ups_params_box"]              = self.__widgets["interface"].vbox6
        self.__widgets["ups_infos"]                   = self.__widgets["interface"].notebook1
        self.__widgets["ups_vars_tree"]               = self.__widgets["interface"].treeview1
        self.__widgets["ups_vars_refresh"]            = self.__widgets["interface"].button9
        self.__widgets["ups_status_image"]            = self.__widgets["interface"].image1
        self.__widgets["ups_status_left"]             = self.__widgets["interface"].label10
        self.__widgets["ups_status_right"]            = self.__widgets["interface"].label11
        self.__widgets["ups_status_time"]             = self.__widgets["interface"].label15
        self.__widgets["menu_favorites_root"]         = self.__widgets["interface"].menu2
        self.__widgets["menu_favorites"]              = self.__widgets["interface"].menu2
        self.__widgets["menu_favorites_add"]          = self.__widgets["interface"].menuitem4
        self.__widgets["menu_favorites_del"]          = self.__widgets["interface"].menuitem5
        self.__widgets["progress_battery_charge"]     = self.__widgets["interface"].progressbar1
        self.__widgets["progress_battery_load"]       = self.__widgets["interface"].progressbar2

        # Create the tray icon and connect it to the show/hide method...
        self.__widgets["status_icon"] = QSystemTrayIcon( QIcon( self.__find_res_file( "pixmaps", "on_line.png" ) ) )
        self.__widgets["status_icon"].setVisible( True )
        self.__widgets["status_icon"].activated.connect( self.tray_activated )

        self.__widgets["ups_status_image"].setPixmap( QPixmap( self.__find_res_file( "pixmaps", "on_line.png" ) ) )

        # Connect interface callbacks actions
        self.__app.aboutToQuit.connect( self.quit )
        self.__widgets["interface"].imagemenuitem1.triggered.connect( self.gui_about_dialog )
        self.__widgets["interface"].imagemenuitem5.triggered.connect( self.quit )
        self.__widgets["ups_host_entry"].textChanged.connect( self.__check_gui_fields )
        self.__widgets["ups_authentication_login"].textChanged.connect( self.__check_gui_fields )
        self.__widgets["ups_authentication_password"].textChanged.connect( self.__check_gui_fields )
        self.__widgets["ups_authentication_check"].stateChanged.connect( self.__check_gui_fields )
        self.__widgets["ups_port_entry"].valueChanged.connect( self.__check_gui_fields )
        self.__widgets["ups_refresh_button"].clicked.connect( self.__update_ups_list )
        self.__widgets["ups_connect"].clicked.connect( self.connect_to_ups )
        self.__widgets["ups_disconnect"].clicked.connect( self.disconnect_from_ups )
        self.__widgets["ups_vars_refresh"].clicked.connect( self.__gui_update_ups_vars_view )
        self.__widgets["menu_favorites_add"].triggered.connect( self.__gui_add_favorite )
        self.__widgets["menu_favorites_del"].triggered.connect( self.__gui_delete_favorite )
        self.__widgets["ups_vars_tree"].doubleClicked.connect( self.__gui_ups_vars_selected )

        # Remove the dummy combobox entry on UPS List and Commands
        self.__widgets["ups_list_combo"].removeItem( 0 )

        # Set UPS vars treeview properties -----------------------------
        store = QStandardItemModel( 0, 3, self.__widgets["ups_vars_tree"] )
        self.__widgets["ups_vars_tree"].setModel( store )
        self.__widgets["ups_vars_tree"].setHeaderHidden( False )
        self.__widgets["ups_vars_tree"].setRootIsDecorated( False )

        # Column 0
        store.setHeaderData( 0, Qt.Orientation.Horizontal, '' )

        # Column 1
        store.setHeaderData( 1, Qt.Orientation.Horizontal, _('Var name') )

        # Column 2
        store.setHeaderData( 2, Qt.Orientation.Horizontal, _('Value') )
        self.__widgets["ups_vars_tree"].header().setStretchLastSection( True )

        self.__widgets["ups_vars_tree"].sortByColumn( 1, Qt.SortOrder.AscendingOrder )
        self.__widgets["ups_vars_tree_store"] = store

        self.__widgets["ups_vars_tree"].setMinimumSize( 0, 50 )
        #---------------------------------------------------------------

        # UPS Commands combo box creation ------------------------------
        ups_commands_height = self.__widgets["ups_commands_combo"].size().height() * 2
        self.__widgets["ups_commands_combo"].setMinimumSize(0, ups_commands_height)
        self.__widgets["ups_commands_combo"].setCurrentIndex( 0 )

        self.__widgets["ups_commands_button"].setMinimumSize(0, ups_commands_height)
        self.__widgets["ups_commands_button"].clicked.connect( self.__gui_send_ups_command )

        self.__widgets["ups_commands_combo_store"] = self.__widgets["ups_commands_combo"]
        #---------------------------------------------------------------

        self.gui_init_unconnected()

        if ( cmd_opts.hidden != True ) :
            self.__widgets["main_window"].show()

        # Define favorites path and load favorites
        if ( platform.system() == "Linux" ) :
            self.__favorites_path = os.path.join( os.environ.get("HOME"), ".nut-monitor" )
        elif ( platform.system() == "Windows" ) :
            self.__favorites_path = os.path.join( os.environ.get("USERPROFILE"), "Application Data", "NUT-Monitor" )

        self.__favorites_file = os.path.join( self.__favorites_path, "favorites.ini" )
        self.__parse_favorites()

        self.gui_status_message( _("Welcome to NUT Monitor") )

        if ( cmd_opts.favorite != None ) :
            if ( cmd_opts.favorite in self.__favorites ) :
                self.__gui_load_favorite( fav_name=cmd_opts.favorite )
                self.connect_to_ups()
        else :
            # Try to scan localhost for available ups and connect to it if there is only one
            self.__widgets["ups_host_entry"].setText( "localhost" )
            self.__update_ups_list()
            if self.__widgets["ups_list_combo"].count() == 1:
                self.connect_to_ups()

    def exec( self ) :
        self.__app.exec()

    def __find_res_file( self, ftype, filename ) :
        # print("%s ~ %s => '%s' '%s'\n" % (os.getcwd(), "/usr/share/nut-monitor", ftype, filename))
        filename = os.path.join( ftype, filename )
        # We would be in script dir
        if os.path.exists(filename):
            return filename
        # TODO: Skip checking application directory if installed
        path = os.path.join( "/usr/share/nut-monitor", filename )
        if os.path.exists(path):
            return path
        path = QStandardPaths.locate(QStandardPaths.AppDataLocation, filename)
        if os.path.exists(path):
            return path
        raise RuntimeError("Cannot find %s resource %s" % (ftype, filename))

    def __find_icon_file( self ) :
        filename = 'nut-monitor.png'

        # TODO: Skip checking application directory if installed
        path = os.path.join( "icons", "256x256", filename )
        if os.path.exists(path):
            return path

        path = os.path.join( "/usr/share/nut-monitor", "icons", "256x256", filename )
        if os.path.exists(path):
            return path

        # Normally icons should be installed to OS location by packaging:
        path = QStandardPaths.locate(QStandardPaths.GenericDataLocation, os.path.join( "icons", "hicolor", "256x256", "apps", filename ) )
        if os.path.exists(path):
            return path

        # Fall back to NUT-specific area where `make install` might put them,
        # e.g. /usr/share/nut/nut-monitor/icons/... or some such, if not the
        # same location as checked in first attempt above:
        path = QStandardPaths.locate(QStandardPaths.AppDataLocation, os.path.join( "icons", "hicolor", "256x256", "apps", filename ) )
        if os.path.exists(path):
            return path

        # No banana!
        raise RuntimeError("Cannot find %s resource %s" % ('icon', filename))

    # Check if correct fields are filled to enable connection to the UPS
    def __check_gui_fields( self, widget=None ) :
        # If UPS list contains something, clear it
        if self.__widgets["ups_list_combo"].currentIndex() != -1 :
            self.__widgets["ups_list_combo"].clear()
            self.__widgets["ups_connect"].setEnabled( False )
            self.__widgets["menu_favorites_add"].setEnabled( False )

        # Host/Port selection
        if len( self.__widgets["ups_host_entry"].text() ) > 0 :
            sensitive = True

            # If authentication is selected, check that we have a login and password
            if self.__widgets["ups_authentication_check"].isChecked() :
                if len( self.__widgets["ups_authentication_login"].text() ) == 0 :
                    sensitive = False

                if len( self.__widgets["ups_authentication_password"].text() ) == 0 :
                    sensitive = False

            self.__widgets["ups_refresh_button"].setEnabled( sensitive )
            if not sensitive :
                self.__widgets["ups_connect"].setEnabled( False )
                self.__widgets["menu_favorites_add"].setEnabled( False )
        else :
            self.__widgets["ups_refresh_button"].setEnabled( False )
            self.__widgets["ups_connect"].setEnabled( False )
            self.__widgets["menu_favorites_add"].setEnabled( False )

        # Use authentication fields...
        if self.__widgets["ups_authentication_check"].isChecked() :
            self.__widgets["ups_authentication_frame"].setEnabled( True )
        else :
            self.__widgets["ups_authentication_frame"].setEnabled( False )

        self.gui_status_message()

    #-------------------------------------------------------------------
    # This method is used to show/hide the main window when user clicks on the tray icon
    def tray_activated(self, reason):
        if reason == QSystemTrayIcon.ActivationReason.Trigger:
            win = self.__widgets["main_window"]
            if win.isVisible():
                win.hide()
            else:
                win.show()
                win.raise_()
                win.activateWindow()

    #-------------------------------------------------------------------
    # Change the status icon and tray icon
    def change_status_icon( self, icon="on_line", blink=False ) :
        self.__widgets["status_icon"].setIcon( QIcon( self.__find_res_file( "pixmaps", "%s.png" % icon ) ) )
        self.__widgets["ups_status_image"].setPixmap( QPixmap( self.__find_res_file( "pixmaps", "%s.png" % icon ) ) )
        # TODO self.__widgets["status_icon"].set_blinking( blink )

    #-------------------------------------------------------------------
    # This method connects to the NUT server and retrieve availables UPSes
    # using connection parameters (host, port, login, pass...)
    def __update_ups_list( self, widget=None ) :

        host     = self.__widgets["ups_host_entry"].text()
        port     = int( self.__widgets["ups_port_entry"].value() )
        login    = None
        password = None

        if self.__widgets["ups_authentication_check"].isChecked() :
            login    = self.__widgets["ups_authentication_login"].text()
            password = self.__widgets["ups_authentication_password"].text()

        try :
            nut_handler = PyNUT.PyNUTClient( host=host, port=port, login=login, password=password )
            upses = nut_handler.GetUPSList()

            ups_list = list(key.decode('ascii') for key in upses.keys())
            ups_list.sort()

            # If UPS list contains something, clear it
            self.__widgets["ups_list_combo"].clear()

            for current in ups_list :
                self.__widgets["ups_list_combo"].addItem( current )

            self.__widgets["ups_list_combo"].setCurrentIndex( 0 )

            self.__widgets["ups_connect"].setEnabled( True )
            self.__widgets["menu_favorites_add"].setEnabled( True )

            self.gui_status_message( _("Found {0} devices on {1}").format( len( ups_list ), host ) )

        except :
            error_msg = _("Error connecting to '{0}' ({1})").format( host, sys.exc_info()[1] )
            self.gui_status_message( error_msg )

    #-------------------------------------------------------------------
    # Quit program
    def quit( self, widget=None ) :
        if self.__quitting:
            return
        self.__quitting = True

        # If we are connected to an UPS, disconnect first...
        if self.__widgets.get("main_window") and self.__connected :
            self.gui_status_message( _("Disconnecting from device") )
            self.disconnect_from_ups()

        self.__app.quit()

    #-------------------------------------------------------------------
    # Method called when user wants to add a new favorite entry. It
    # displays a dialog to enable user to select the name of the favorite
    def __gui_add_favorite( self, widget=None ) :
        dialog_ui_file = self.__find_res_file( 'ui', "dialog1.ui" )
        dialog = PyQt6.uic.loadUi( dialog_ui_file )

        # Define interface callbacks actions
        def check_entry(val):
            if self.__gui_add_favorite_check_gui_fields(val):
                dialog.buttonBox.button(QDialogButtonBox.StandardButton.Ok).setEnabled( True )
            else:
                dialog.buttonBox.button(QDialogButtonBox.StandardButton.Ok).setEnabled( False )
        dialog.entry4.textChanged.connect( check_entry )

        self.__widgets["main_window"].setEnabled( False )
        rc = dialog.exec()
        if rc == QDialog.DialogCode.Accepted :
            fav_data = {}
            fav_data["host"] = self.__widgets["ups_host_entry"].text()
            fav_data["port"] = "%d" % self.__widgets["ups_port_entry"].value()
            fav_data["ups"]  = self.__widgets["ups_list_combo"].currentText()
            fav_data["auth"] = self.__widgets["ups_authentication_check"].isChecked()
            if fav_data["auth"] :
                fav_data["login"]    = self.__widgets["ups_authentication_login"].text()
                fav_data["password"] = base64.b64encode( self.__widgets["ups_authentication_password"].text().encode('ascii') ).decode('ascii')

            fav_name = dialog.entry4.text()
            self.__favorites[ fav_name ] = fav_data
            self.__gui_refresh_favorites_menu()

            # Save all favorites
            self.__save_favorites()

        self.__widgets["main_window"].setEnabled( True )

    #-------------------------------------------------------------------
    # Method called when user wants to delete an entry from favorites
    def __gui_delete_favorite( self, widget=None ) :
        dialog_ui_file = self.__find_res_file( 'ui', "dialog2.ui" )
        dialog = PyQt6.uic.loadUi( dialog_ui_file )

        # Remove the dummy combobox entry on list
        dialog.combobox2.removeItem( 0 )

        favs = list(self.__favorites.keys())
        favs.sort()
        for current in favs :
            dialog.combobox2.addItem( current )

        dialog.combobox2.setCurrentIndex( 0 )

        self.__widgets["main_window"].setEnabled( False )
        rc = dialog.exec()
        fav_name = dialog.combobox2.currentText()
        self.__widgets["main_window"].setEnabled( True )

        if ( rc == QDialog.DialogCode.Accepted ) :
            # Remove entry, show confirmation dialog
            resp = QMessageBox.question( None, self.__widgets["main_window"].windowTitle(), _("Are you sure that you want to remove this favorite ?"), QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,  QMessageBox.StandardButton.Yes )
            if ( resp == QMessageBox.StandardButton.Yes ) :
                del self.__favorites[ fav_name ]
                self.__gui_refresh_favorites_menu()
                self.__save_favorites()
                self.gui_status_message( _("Removed favorite '%s'") % fav_name )

    #-------------------------------------------------------------------
    # Method called when user selects a favorite from the favorites menu
    def __gui_load_favorite( self, fav_name="" ) :

        if ( fav_name in self.__favorites ) :
            # If auth is activated, process it before other fields to avoir weird
            # reactions with the 'check_gui_fields' function.
            if ( self.__favorites[fav_name].get("auth", False ) ) :
                self.__widgets["ups_authentication_check"].setChecked( True )
                self.__widgets["ups_authentication_login"].setText( self.__favorites[fav_name].get("login","") )
                self.__widgets["ups_authentication_password"].setText( self.__favorites[fav_name].get("password","") )

            self.__widgets["ups_host_entry"].setText( self.__favorites[fav_name].get("host","") )
            self.__widgets["ups_port_entry"].setValue( int( self.__favorites[fav_name].get( "port", 3493 ) ) )

            # Clear UPS list and add current UPS name
            self.__widgets["ups_list_combo"].clear()

            self.__widgets["ups_list_combo"].addItem( self.__favorites[fav_name].get("ups","") )
            self.__widgets["ups_list_combo"].setCurrentIndex( 0 )

            # Activate the connect button
            self.__widgets["ups_connect"].setEnabled( True )

            self.gui_status_message( _("Loaded '%s'") % fav_name )

    #-------------------------------------------------------------------
    # Send the selected command to the UPS
    def __gui_send_ups_command( self, widget=None ) :
        offset = self.__widgets["ups_commands_combo"].currentIndex()
        cmd    = self.__ups_commands[ offset ].decode('ascii')

        self.__widgets["main_window"].setEnabled( False )
        resp = QMessageBox.question( None, self.__widgets["main_window"].windowTitle(), _("Are you sure that you want to send '%s' to the device ?") % cmd, QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.Yes )
        self.__widgets["main_window"].setEnabled( True )

        if ( resp == QMessageBox.StandardButton.Yes ) :
            try :
                self.__ups_handler.RunUPSCommand( self.__current_ups, cmd )
                self.gui_status_message( _("Sent '{0}' command to {1}").format( cmd, self.__current_ups ) )

            except :
                self.gui_status_message( _("Failed to send '{0}' ({1})").format( cmd, sys.exc_info()[1] ) )

    #-------------------------------------------------------------------
    # Method called when user clicks on the UPS vars treeview. If the user
    # performs a double click on a RW var, the GUI shows the update var dialog.
    def __gui_ups_vars_selected( self, index ) :
        if True :
            model = self.__widgets["ups_vars_tree_store"]
            try :
                ups_var = model.data( index.siblingAtColumn(1) ).encode('ascii')
                if ( ups_var in self.__ups_rw_vars ) :
                    # The selected var is RW, then we can show the update dialog

                    cur_val = self.__ups_rw_vars.get(ups_var).decode('ascii')

                    self.__widgets["main_window"].setEnabled( False )

                    new_val, ok = QInputDialog.getText(
                        self.__widgets["main_window"],
                        _("Edit Variable"),
                        _("Enter a new value for the variable:\n\n{0} = {1} (current value)").format( ups_var.decode('ascii'), cur_val ),
                        QLineEdit.EchoMode.Normal,
                        cur_val
                    )

                    self.__widgets["main_window"].setEnabled( True )

                    if ( ok ):
                        try :
                            self.__ups_handler.SetRWVar( ups=self.__current_ups, var=ups_var.decode('ascii'), value=new_val )
                            self.gui_status_message( _("Updated variable on %s") % self.__current_ups )

                            # Change the value on the local dict to update the GUI
                            new_val = new_val.encode('ascii')
                            self.__ups_vars[ups_var]    = new_val
                            self.__ups_rw_vars[ups_var] = new_val
                            self.__gui_update_ups_vars_view()

                        except :
                            error_msg = _("Error updating variable on '{0}' ({1})").format( self.__current_ups, sys.exc_info()[1] )
                            self.gui_status_message( error_msg )

                    else :
                        # User cancelled modification...
                        error_msg = _("No variable modified on %s - User cancelled") % self.__current_ups
                        self.gui_status_message( error_msg )

            except :
                # Failed to get information from the treeview... skip action
                pass

    #-------------------------------------------------------------------
    # Refresh the content of the favorites menu according to the defined favorites
    def __gui_refresh_favorites_menu( self ) :
        for current in self.__fav_menu_items :
            self.__widgets["menu_favorites"].removeAction(current)

        self.__fav_menu_items = list()

        items = list(self.__favorites.keys())
        items.sort()

        for current in items :
            menu_item = QAction( current, self.__widgets["main_window"] )
            self.__fav_menu_items.append( menu_item )
            self.__widgets["menu_favorites"].addAction( menu_item )

            menu_item.triggered.connect( lambda _, name=current: self.__gui_load_favorite(name) )

        if len( items ) > 0 :
            self.__widgets["menu_favorites_del"].setEnabled( True )
        else :
            self.__widgets["menu_favorites_del"].setEnabled( False )

    #-------------------------------------------------------------------
    # In 'add favorites' dialog, this method compares the content of the
    # text widget representing the name of the new favorite with existing
    # ones. If they match, the 'add' button will be set to non sensitive
    # to avoid creating entries with the same name.
    def __gui_add_favorite_check_gui_fields( self, fav_name ) :
        if ( len( fav_name ) > 0 ) and ( fav_name not in list(self.__favorites.keys()) ) :
            return True
        else :
            return False

    #-------------------------------------------------------------------
    # Load and parse favorites
    def __parse_favorites( self ) :

        if ( not os.path.exists( self.__favorites_file ) ) :
            # There is no favorites files, do nothing
            return

        try :
            if ( not stat.S_IMODE( os.stat( self.__favorites_path ).st_mode ) == self.DESIRED_FAVORITES_DIRECTORY_MODE ) : # unsafe pre-1.2 directory found
                os.chmod( self.__favorites_path, self.DESIRED_FAVORITES_DIRECTORY_MODE )

            conf = configparser.ConfigParser()
            conf.read( self.__favorites_file )
            for current in conf.sections() :
                # Check if mandatory fields are present
                if ( conf.has_option( current, "host" ) and conf.has_option( current, "ups" ) ) :
                    # Valid entry found, add it to the list
                    fav_data = {}
                    fav_data["host"] = conf.get( current, "host" )
                    fav_data["ups"]  = conf.get( current, "ups" )

                    if ( conf.has_option( current, "port" ) ) :
                        fav_data["port"] = conf.get( current, "port" )
                    else :
                        fav_data["port"] = "3493"

                    # If auth is defined the section must have login and pass defined
                    if ( conf.has_option( current, "auth" ) ) :
                        if( conf.has_option( current, "login" ) and conf.has_option( current, "password" ) ) :
                            # Add the entry
                            fav_data["auth"]     = conf.getboolean( current, "auth" )
                            fav_data["login"]    = conf.get( current, "login" )

                            try :
                                fav_data["password"] = base64.decodebytes( conf.get( current, "password" ).encode('ascii') ).decode('ascii')

                            except :
                                # If the password is not in base64, let the field empty
                                print(( _("Error parsing favorites, password for '%s' is not in base64\nSkipping password for this entry") % current ))
                                fav_data["password"] = ""
                    else :
                        fav_data["auth"] = False

                    self.__favorites[current] = fav_data
            self.__gui_refresh_favorites_menu()

        except :
            self.gui_status_message( _("Error while parsing favorites file (%s)") % sys.exc_info()[1] )

    #-------------------------------------------------------------------
    # Save favorites to the defined favorites file using ini format
    def __save_favorites( self ) :

        # If path does not exists, try to create it
        if ( not os.path.exists( self.__favorites_file ) ) :
            try :
                os.makedirs( self.__favorites_path, mode=self.DESIRED_FAVORITES_DIRECTORY_MODE, exist_ok=True )
            except :
                self.gui_status_message( _("Error while creating configuration folder (%s)") % sys.exc_info()[1] )

        save_conf = configparser.ConfigParser()
        for current in list(self.__favorites.keys()) :
            save_conf.add_section( current )
            for k, v in self.__favorites[ current ].items() :
                if isinstance( v, bool ) :
                    v = str( v )
                save_conf.set( current, k, v )

        try :
            fh = open( self.__favorites_file, "w" )
            save_conf.write( fh )
            fh.close()
            self.gui_status_message( _("Saved favorites...") )

        except :
            self.gui_status_message( _("Error while saving favorites (%s)") % sys.exc_info()[1] )

    #-------------------------------------------------------------------
    # Display the about dialog
    def gui_about_dialog( self, widget=None ) :
        self.__widgets["main_window"].adjustSize()
        dialog_ui_file = self.__find_res_file( 'ui', "aboutdialog1.ui" )

        dialog = PyQt6.uic.loadUi( dialog_ui_file )
        dialog.icon.setPixmap( QPixmap( self.__find_icon_file() ) )
        ### sys.stderr.write("gui_about_dialog(): dialog=%s\n" % str(dir(dialog)))
        ### sys.stderr.write("gui_about_dialog(): label=%s\n" % str(dir(dialog.label)))
        ### sys.stderr.write("gui_about_dialog(): text=%s\n" % str(dir(dialog.label.text)))
        s = dialog.label.text()
        ### sys.stderr.write("gui_about_dialog(): s (original) ='%s'\n" % s)
        s = _(s)
        ### sys.stderr.write("gui_about_dialog(): s (localized) ='%s'\n" % s)
        s = (s
             .replace('%s%s%s' % ("@", "PACKAGE_VERSION", "@"), '2.8.4')
             .replace('%s%s%s' % ("@", "NUT_WEBSITE_BASE", "@"), 'https://www.networkupstools.org/historic/v2.8.4')
            )
        ### sys.stderr.write("gui_about_dialog(): s (rewritten) ='%s'\n" % s)
        dialog.label.setText(s)

        credits_button = QPushButton( dialog )
        credits_button.setText( _("C&redits") )
        credits_button.setIcon( dialog.style().standardIcon( QStyle.StandardPixmap.SP_MessageBoxInformation ) )
        credits_button.clicked.connect( self.gui_about_credits )

        licence_button = QPushButton( dialog )
        licence_button.setText( _("&Licence") )
        licence_button.clicked.connect( self.gui_about_licence )

        dialog.buttonBox.addButton( credits_button, QDialogButtonBox.ButtonRole.HelpRole )
        dialog.buttonBox.addButton( licence_button, QDialogButtonBox.ButtonRole.HelpRole )

        self.__widgets["main_window"].setEnabled( False )
        dialog.exec()
        self.__widgets["main_window"].setEnabled( True )

    def gui_about_credits( self ) :
        QMessageBox.about( None, _("Credits"), _("""
Written by:
David Goncalves <david@lestat.st>

Translated by:
David Goncalves <david@lestat.st> - Français
Daniele Pezzini <hyouko@gmail.com> - Italiano
Alexey Rodionov <alexey.rodionov@red-soft.ru> - Russian
        """).strip() )

    def gui_about_licence( self ) :
        QMessageBox.about( None, _("Licence"), _("""
Copyright (C) 2010 David Goncalves <david@lestat.st>
Copyright (C) since 2010 by NUT Community

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 <http://www.gnu.org/licenses/>.
        """).strip() )

    #-------------------------------------------------------------------
    # Display a message on the status bar. The message is also set as
    # tooltip to enable users to see long messages.
    def gui_status_message( self, msg="" ) :
        text = msg

        self.__widgets["status_bar"].showMessage( text.replace("\n", "") )
        self.__widgets["status_bar"].setToolTip( text )

    #-------------------------------------------------------------------
    # Display a notification using QSystemTrayIcon with an optional icon
    def gui_status_notification( self, message="", icon_file="" ) :
        if ( icon_file != "" ) :
            icon = QIcon( os.path.abspath( self.__find_res_file( "pixmaps", icon_file ) ) )
        else :
            icon = None

        self.__widgets["status_icon"].showMessage(
            "NUT Monitor",
            message,
            icon if icon else QSystemTrayIcon.MessageIcon.Information,
            5000
        )

    #-------------------------------------------------------------------
    # Connect to the selected UPS using parameters (host,port,login,pass)
    def connect_to_ups( self, widget=None ) :

        host     = self.__widgets["ups_host_entry"].text()
        port     = int( self.__widgets["ups_port_entry"].value() )
        login    = None
        password = None

        if self.__widgets["ups_authentication_check"].isChecked() :
            login    = self.__widgets["ups_authentication_login"].text()
            password = self.__widgets["ups_authentication_password"].text()

        try :
            self.__ups_handler = PyNUT.PyNUTClient( host=host, port=port, login=login, password=password )

        except :
            self.gui_status_message( _("Error connecting to '{0}' ({1})").format( host, sys.exc_info()[1] ) )
            self.gui_status_notification( _("Error connecting to '{0}'\n{1}").format( host, sys.exc_info()[1] ), "warning.png" )
            return

        # Check if selected UPS exists on server...
        srv_upses          = self.__ups_handler.GetUPSList()
        self.__current_ups = self.__widgets["ups_list_combo"].currentText()

        if self.__current_ups.encode('ascii') not in srv_upses :
            self.gui_status_message( _("Device '%s' not found on server") % self.__current_ups )
            self.gui_status_notification( _("Device '%s' not found on server") % self.__current_ups, "warning.png" )
            return

        if not self.__ups_handler.CheckUPSAvailable( self.__current_ups ):
            self.gui_status_message( _("UPS '{0}' is not reachable").format( self.__current_ups ) )
            self.gui_status_notification( _("UPS '{0}' is not reachable").format( self.__current_ups ), "warning.png" )
            return

        self.__connected = True
        self.__widgets["ups_connect"].hide()
        self.__widgets["ups_disconnect"].show()
        self.__widgets["ups_infos"].show()
        self.__widgets["ups_params_box"].setEnabled( False )
        self.__widgets["menu_favorites_root"].setEnabled( False )
        self.__widgets["ups_params_box"].hide()

        commands = self.__ups_handler.GetUPSCommands( self.__current_ups )
        self.__ups_commands = list(commands.keys())
        self.__ups_commands.sort()

        # Refresh UPS commands combo box
        self.__widgets["ups_commands_combo_store"].clear()
        for desc in self.__ups_commands :
            # TODO: Style as "%s<br><font color=\"#707070\">%s</font>"
            self.__widgets["ups_commands_combo_store"].addItem( "%s\n%s" % ( desc.decode('ascii'), commands[desc].decode('ascii') ) )

        self.__widgets["ups_commands_combo"].setCurrentIndex( 0 )

        # Update UPS vars manually before the updater
        self.__ups_vars    = self.__ups_handler.GetUPSVars( self.__current_ups )
        self.__ups_rw_vars = self.__ups_handler.GetRWVars( self.__current_ups )
        self.__gui_update_ups_vars_view()

        # Try to resize the main window...
        # FIXME: For some reason, calling this immediately doesn't work right
        QTimer.singleShot(10, self.__widgets["main_window"].adjustSize)

        # Start the GUI updater
        self.__gui_updater = gui_updater( self )
        self.__gui_updater.start()

        self.gui_status_message( _("Connected to '{0}' on {1}").format( self.__current_ups, host ) )


    #-------------------------------------------------------------------
    # Refresh UPS vars in the treeview
    def __gui_update_ups_vars_view( self, widget=None ) :
        if self.__ups_handler :
            vars   = self.__ups_vars
            rwvars = self.__ups_rw_vars

            self.__widgets["ups_vars_tree_store"].removeRows(0, self.__widgets["ups_vars_tree_store"].rowCount())

            for k,v in vars.items() :
                if ( k in rwvars ) :
                    icon_file = self.__find_res_file( "pixmaps", "var-rw.png" )
                else :
                    icon_file = self.__find_res_file( "pixmaps", "var-ro.png" )

                icon = QIcon( icon_file )
                item_icon = QStandardItem(icon, '')
                item_icon.setFlags(Qt.ItemFlag.ItemIsSelectable | Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemNeverHasChildren)
                item_var_name = QStandardItem( k.decode('ascii') )
                item_var_name.setFlags(Qt.ItemFlag.ItemIsSelectable | Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemNeverHasChildren)
                item_var_val = QStandardItem( v.decode('ascii') )
                item_var_val.setFlags(Qt.ItemFlag.ItemIsSelectable | Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemNeverHasChildren)
                self.__widgets["ups_vars_tree_store"].appendRow( (item_icon, item_var_name, item_var_val) )
            self.__widgets["ups_vars_tree"].resizeColumnToContents( 0 )
            self.__widgets["ups_vars_tree"].resizeColumnToContents( 1 )


    def gui_init_unconnected( self ) :
        if self.__quitting:
            return

        if not self.__widgets.get("main_window") or not self.__widgets.get("status_icon"):
            return

        self.__connected = False
        self.__widgets["ups_connect"].show()
        self.__widgets["ups_disconnect"].hide()
        self.__widgets["ups_infos"].hide()
        self.__widgets["ups_params_box"].setEnabled( True )
        self.__widgets["menu_favorites_root"].setEnabled( True )
        self.__widgets["status_icon"].setToolTip( _("<i>Not connected</i>") )
        self.__widgets["ups_params_box"].show()

        # Try to resize the main window...
        self.__widgets["main_window"].adjustSize()

    #-------------------------------------------------------------------
    # Disconnect from the UPS
    def disconnect_from_ups( self, widget=None ) :
        self.gui_init_unconnected()

        # Stop the GUI updater
        self.__gui_updater.stop()
        self.__gui_updater = None

        del self.__ups_handler
        self.gui_status_message( _("Disconnected from '%s'") % self.__current_ups )
        self.change_status_icon( "on_line", blink=False )
        self.__current_ups = None

#-----------------------------------------------------------------------
# GUI Updater class
# This class updates the main gui with data from connected UPS
class gui_updater :

    __parent_class = None
    __stop_updater = False

    def __init__( self, parent_class ) :
        self.__parent_class = parent_class

    def start( self ) :
        self.__timer = QTimer()
        self.__timer.timeout.connect(self.__update)
        self.__timer.start(1000)

    def __update( self ) :

        ups    = self.__parent_class._interface__current_ups

        # Define a dict containing different UPS status
        status_mapper = { b"LB"     : "<font color=\"#BB0000\"><b>%s</b></font>" % _("Low batteries"),
                          b"RB"     : "<font color=\"#FF0000\"><b>%s</b></font>" % _("Replace batteries !"),
                          b"ALARM"  : "<font color=\"#FF0000\"><b>%s</b></font>" % _("Active alarms !"),
                          b"BYPASS" : "<font color=\"#BB0000\">Bypass</font> <i>%s</i>" % _("(no battery protection)"),
                          b"ECO"    : _("In ECO mode (as defined by vendor)"),
                          b"CAL"    : _("Performing runtime calibration"),
                          b"OFF"    : "<font color=\"#000090\">%s</font> <i>(%s)</i>" % ( _("Offline"), _("not providing power to the load") ),
                          b"OVER"   : "<font color=\"#BB0000\">%s</font> <i>(%s)</i>" % ( _("Overloaded !"), _("there is too much load for device") ),
                          b"TRIM"   : _("Triming <i>(UPS is triming incoming voltage)</i>"),
                          b"BOOST"  : _("Boost <i>(UPS is boosting incoming voltage)</i>")
                        }

        if ups and not self.__stop_updater and not self.__parent_class._interface__quitting :
            try :
                vars = self.__parent_class._interface__ups_handler.GetUPSVars( ups )
                self.__parent_class._interface__ups_vars = vars

                # Text displayed on the status frame
                text_left   = ""
                text_right  = ""
                status_text = ""

                text_left  += "<b>%s</b><br>" % _("Device status :")

                if ( vars.get(b"ups.status").find(b"OL") != -1 ) :
                    text_right += "<font color=\"#009000\"><b>%s</b></font>" % _("Online")
                    if self.__parent_class._interface__online is not True :
                        self.__parent_class.change_status_icon( "on_line", blink=False )
                        self.__parent_class.gui_status_notification( _("Device is online"), "on_line.png" )
                        self.__parent_class._interface__online = True

                if ( vars.get(b"ups.status").find(b"OB") != -1 ) :
                    text_right += "<font color=\"#900000\"><b>%s</b></font>" % _("On batteries")
                    if self.__parent_class._interface__online is not False:
                        self.__parent_class.change_status_icon( "on_battery", blink=True )
                        self.__parent_class.gui_status_notification( _("Device is running on batteries"), "on_battery.png" )
                        self.__parent_class._interface__online = False

                # Check for additionnal information
                for k,v in status_mapper.items() :
                    if vars.get(b"ups.status").find(k) != -1 :
                        if ( text_right != "" ) :
                            text_right += " - %s" % v
                        else :
                            text_right += "%s" % v

                # CHRG and DISCHRG cannot be trated with the previous loop ;)
                if ( vars.get(b"ups.status").find(b"DISCHRG") != -1 ) :
                    text_right += " - <i>%s</i>" % _("discharging")
                elif ( vars.get(b"ups.status").find(b"CHRG") != -1 ) :
                    text_right += " - <i>%s</i>" % _("charging")

                status_text += text_right
                text_right += "<br>"

                if ( b"ups.mfr" in vars ) :
                    text_left  += "<b>%s</b><br><br>" % _("Model :")
                    text_right += "%s<br>%s<br>" % (
                        vars.get(b"ups.mfr",b"").decode('ascii'),
                        vars.get(b"ups.model",b"").decode('ascii'),
                    )

                if ( b"ups.temperature" in vars ) :
                    text_left  += "<b>%s</b><br>" % _("Temperature :")
                    text_right += "%s<br>" % int( float( vars.get( b"ups.temperature", 0 ) ) )

                if ( b"battery.voltage" in vars ) :
                    text_left  += "<b>%s</b><br>" % _("Battery voltage :")
                    text_right += "%sv<br>" % (vars.get( b"battery.voltage", 0 ).decode('ascii'),)

                self.__parent_class._interface__widgets["ups_status_left"].setText( text_left[:-4] )
                self.__parent_class._interface__widgets["ups_status_right"].setText( text_right[:-4] )

                # UPS load and battery charge progress bars
                self.__parent_class._interface__widgets["progress_battery_charge"].setRange( 0, 100 )
                if ( b"battery.charge" in vars ) :
                    charge = vars.get( b"battery.charge", "0" )
                    self.__parent_class._interface__widgets["progress_battery_charge"].setValue( int( float( charge ) ) )
                    self.__parent_class._interface__widgets["progress_battery_charge"].resetFormat()
                    status_text += "<br>%s %s%%" % ( _("Battery charge :"), int( float( charge ) ) )
                else :
                    self.__parent_class._interface__widgets["progress_battery_charge"].setValue( 0 )
                    self.__parent_class._interface__widgets["progress_battery_charge"].setFormat( _("Not available") )
                    # FIXME: Some themes don't draw text, so swap it with a QLabel?

                self.__parent_class._interface__widgets["progress_battery_load"].setRange( 0, 100 )
                if ( b"ups.load" in vars ) :
                    load = vars.get( b"ups.load", "0" )
                    self.__parent_class._interface__widgets["progress_battery_load"].setValue( int( float( load ) ) )
                    self.__parent_class._interface__widgets["progress_battery_load"].resetFormat()
                    status_text += "<br>%s %s%%" % ( _("UPS load :"), int( float( load ) ) )
                else :
                    self.__parent_class._interface__widgets["progress_battery_load"].setValue( 0 )
                    self.__parent_class._interface__widgets["progress_battery_load"].setFormat( _("Not available") )
                    # FIXME: Some themes don't draw text, so swap it with a QLabel?

                if ( b"battery.runtime" in vars ) :
                    autonomy = int( float( vars.get( b"battery.runtime", 0 ) ) )

                    if ( autonomy >= 3600 ) :
                        info = time.strftime( _("<b>%H hours %M minutes %S seconds</b>"), time.gmtime( autonomy ) )
                    elif ( autonomy > 300 ) :
                        info = time.strftime( _("<b>%M minutes %S seconds</b>"), time.gmtime( autonomy ) )
                    else :
                        info = time.strftime( _("<b><font color=\"#DD0000\">%M minutes %S seconds</font></b>"), time.gmtime( autonomy ) )
                else :
                    info = _("Not available")

                self.__parent_class._interface__widgets["ups_status_time"].setText( info )

                # Display UPS status as tooltip for tray icon
                self.__parent_class._interface__widgets["status_icon"].setToolTip( status_text )

            except :
                self.__parent_class._interface__online = None
                self.__parent_class.change_status_icon( "warning", blink=True )
                self.__parent_class.gui_status_message( _("Error from '{0}' ({1})").format( ups, sys.exc_info()[1] ) )
                self.__parent_class.gui_status_notification( _("Error from '{0}'\n{1}").format( ups, sys.exc_info()[1] ), "warning.png" )

    def stop( self ) :
        self.__timer.stop()
        self.__stop_updater = True


#-----------------------------------------------------------------------
# The main program starts here :-)
if __name__ == "__main__" :

    # Init the localisation
    APP = "NUT-Monitor"
    DIR = "locale"

    gettext.bindtextdomain( APP, DIR )
    gettext.textdomain( APP )
    _ = gettext.gettext

    for module in ( gettext, ) :
         module.bindtextdomain( APP, DIR )
         module.textdomain( APP )

    gui = interface(sys.argv)
    gui.exec()
