944 lines
44 KiB
Text
Executable file
944 lines
44 KiB
Text
Executable file
#!@PYTHON3@
|
|
# -*- 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.
|
|
|
|
|
|
import PyQt5.uic
|
|
from PyQt5.QtCore import *
|
|
from PyQt5.QtGui import *
|
|
from PyQt5.QtWidgets import *
|
|
import sys
|
|
import base64
|
|
import os, os.path
|
|
import stat
|
|
import platform
|
|
import time
|
|
import threading
|
|
import optparse
|
|
import configparser
|
|
import locale
|
|
import gettext
|
|
import PyNUT
|
|
|
|
|
|
class interface :
|
|
|
|
DESIRED_FAVORITES_DIRECTORY_MODE = 0o700
|
|
|
|
__widgets = {}
|
|
__callbacks = {}
|
|
__favorites = {}
|
|
__favorites_file = None
|
|
__favorites_path = ""
|
|
__fav_menu_items = list()
|
|
__window_visible = True
|
|
__ui_file = None
|
|
__connected = False
|
|
__ups_handler = None
|
|
__ups_commands = None
|
|
__ups_vars = None
|
|
__ups_rw_vars = None
|
|
__gui_thread = None
|
|
__current_ups = None
|
|
|
|
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 )
|
|
|
|
self.__ui_file = self.__find_res_file( 'ui', "window1.ui" )
|
|
|
|
self.__widgets["interface"] = PyQt5.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.__widgets["main_window"].destroyed.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.Horizontal, '' )
|
|
|
|
# Column 1
|
|
store.setHeaderData( 1, Qt.Horizontal, _('Var name') )
|
|
|
|
# Column 2
|
|
store.setHeaderData( 2, Qt.Horizontal, _('Value') )
|
|
self.__widgets["ups_vars_tree"].header().setStretchLastSection( True )
|
|
|
|
self.__widgets["ups_vars_tree"].sortByColumn( 1, Qt.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 ) :
|
|
filename = os.path.join( ftype, filename )
|
|
# TODO: Skip checking application directory if installed
|
|
path = os.path.join( os.path.dirname( sys.argv[0] ), 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( os.path.dirname( sys.argv[0] ), "icons", "256x256", filename )
|
|
if os.path.exists(path):
|
|
return path
|
|
path = QStandardPaths.locate(QStandardPaths.AppDataLocation, os.path.join( "icons", "hicolor", "256x256", "apps", filename ) )
|
|
if os.path.exists(path):
|
|
return path
|
|
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, widget=None, data=None ) :
|
|
if self.__window_visible :
|
|
self.__widgets["main_window"].hide()
|
|
else :
|
|
self.__widgets["main_window"].show()
|
|
|
|
self.__window_visible = not self.__window_visible
|
|
|
|
#-------------------------------------------------------------------
|
|
# 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 we are connected to an UPS, disconnect first...
|
|
if 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 = PyQt5.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.Ok).setEnabled( True )
|
|
else:
|
|
dialog.buttonBox.button(QDialogButtonBox.Ok).setEnabled( False )
|
|
dialog.entry4.textChanged.connect( check_entry )
|
|
|
|
self.__widgets["main_window"].setEnabled( False )
|
|
rc = dialog.exec()
|
|
if rc == QDialog.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 = PyQt5.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.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.Yes | QMessageBox.No, QMessageBox.Yes )
|
|
|
|
if ( resp == QMessageBox.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.Yes | QMessageBox.No, QMessageBox.Yes )
|
|
self.__widgets["main_window"].setEnabled( True )
|
|
|
|
if ( resp == QMessageBox.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, rc = QInputDialog.getText( None, self.__widgets["main_window"].windowTitle(), _("Enter a new value for the variable.<br><br>{0} = {1} <font color=\"#606060\"><i>(current value)</i></font>").format( ups_var, cur_val), QLineEdit.Normal, cur_val )
|
|
self.__widgets["main_window"].setEnabled( True )
|
|
|
|
if ( rc ) :
|
|
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.__fav_menu_items.append( menu_item )
|
|
self.__widgets["menu_favorites"].addAction( menu_item )
|
|
|
|
menu_item.triggered.connect( lambda: self.__gui_load_favorite( current ) )
|
|
|
|
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 = PyQt5.uic.loadUi( dialog_ui_file )
|
|
dialog.icon.setPixmap( QPixmap( self.__find_icon_file() ) )
|
|
|
|
credits_button = QPushButton( dialog )
|
|
credits_button.setText( _("C&redits") )
|
|
credits_button.setIcon( dialog.style().standardIcon( QStyle.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.HelpRole )
|
|
dialog.buttonBox.addButton( licence_button, QDialogButtonBox.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
|
|
""").strip() )
|
|
|
|
def gui_about_licence( self ) :
|
|
QMessageBox.about( None, _("Licence"), _("""
|
|
Copyright (C) 2010 David Goncalves <david@lestat.st>
|
|
|
|
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
|
|
|
|
message_id = 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 )
|
|
|
|
#-------------------------------------------------------------------
|
|
# 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
|
|
|
|
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 thread
|
|
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 thread
|
|
self.__gui_thread = gui_updater( self )
|
|
self.__gui_thread.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.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemNeverHasChildren)
|
|
item_var_name = QStandardItem( k.decode('ascii') )
|
|
item_var_name.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemNeverHasChildren)
|
|
item_var_val = QStandardItem( v.decode('ascii') )
|
|
item_var_val.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.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 ) :
|
|
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 thread
|
|
self.__gui_thread.stop_thread()
|
|
|
|
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_thread = False
|
|
|
|
def __init__( self, parent_class ) :
|
|
threading.Thread.__init__( self )
|
|
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
|
|
was_online = True
|
|
|
|
# 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"BYPASS" : "<font color=\"#BB0000\">Bypass</font> <i>%s</i>" % _("(no battery protection)"),
|
|
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 not self.__stop_thread :
|
|
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 not was_online :
|
|
self.__parent_class.change_status_icon( "on_line", blink=False )
|
|
was_online = True
|
|
|
|
if ( vars.get(b"ups.status").find(b"OB") != -1 ) :
|
|
text_right += "<font color=\"#900000\"><b>%s</b></font>" % _("On batteries")
|
|
if was_online :
|
|
self.__parent_class.change_status_icon( "on_battery", blink=True )
|
|
self.__parent_class.gui_status_notification( _("Device is running on batteries"), "on_battery.png" )
|
|
was_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.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_thread( self ) :
|
|
self.__timer.stop()
|
|
|
|
|
|
#-----------------------------------------------------------------------
|
|
# 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()
|
|
|