@@ -43,6 +43,7 @@ from PyQt5.QtCore import *
4343from PyQt5.QtGui import *
4444from PyQt5.QtWidgets import *
4545import sys
46+ import socket
4647import base64
4748import os, os.path
4849import stat
@@ -53,6 +54,7 @@ import optparse
5354import configparser
5455import locale
5556import gettext
57+ import PyNUT
5658
5759# Try local development/accompanying packaging first,
5860# then system installation
@@ -407,8 +409,11 @@ class interface :
407409 def quit( self, widget=None ) :
408410 # If we are connected to an UPS, disconnect first...
409411 if self.__connected :
410- self.gui_status_message( _("Disconnecting from device") )
411- self.disconnect_from_ups()
412+ try:
413+ self.gui_status_message( _("Disconnecting from device") )
414+ self.disconnect_from_ups()
415+ except RuntimeError:
416+ pass # widgets may already be deleted during shutdown
412417
413418 self.__app.quit()
414419
@@ -748,20 +753,28 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
748753 # Display a message on the status bar. The message is also set as
749754 # tooltip to enable users to see long messages.
750755 def gui_status_message( self, msg="" ) :
751- text = msg
752-
753- message_id = self.__widgets["status_bar"].showMessage( text.replace("\n", "") )
754- self.__widgets["status_bar"].setToolTip( text )
756+ try:
757+ text = msg
758+ if self.__widgets.get("status_bar"):
759+ self.__widgets["status_bar"].showMessage( text.replace("\n", "") )
760+ self.__widgets["status_bar"].setToolTip( text )
761+ except Exception:
762+ pass
755763
756764 #-------------------------------------------------------------------
757765 # Display a notification using QSystemTrayIcon with an optional icon
758766 def gui_status_notification( self, message="", icon_file="" ) :
759- if ( icon_file != "" ) :
760- icon = QIcon( os.path.abspath( self.__find_res_file( "pixmaps", icon_file ) ) )
761- else :
762- icon = None
763-
764- self.__widgets["status_icon"].showMessage( "NUT Monitor", message, icon )
767+ try:
768+ if not self.__widgets.get("status_icon"):
769+ return
770+ if ( icon_file != "" ) :
771+ icon = QIcon( os.path.abspath( self.__find_res_file( "pixmaps", icon_file ) ) )
772+ else :
773+ icon = None
774+
775+ self.__widgets["status_icon"].showMessage( "NUT Monitor", message, icon )
776+ except Exception:
777+ pass
765778
766779 #-------------------------------------------------------------------
767780 # Connect to the selected UPS using parameters (host,port,login,pass)
@@ -895,6 +908,7 @@ class gui_updater :
895908 __parent_class = None
896909 __stop_thread = False
897910 was_online = True
911+ __reconnect_attempted = False
898912
899913 def __init__( self, parent_class ) :
900914 threading.Thread.__init__( self )
@@ -1031,12 +1045,101 @@ class gui_updater :
10311045 # Display UPS status as tooltip for tray icon
10321046 self.__parent_class._interface__widgets["status_icon"].setToolTip( "\n".join( tooltip_lines ) )
10331047
1034- except :
1035- self.__parent_class.gui_status_message( _("Error from '{0}' ({1})").format( ups, sys.exc_info()[1] ) )
1036- self.__parent_class.gui_status_notification( _("Error from '{0}'\n{1}").format( ups, sys.exc_info()[1] ), "warning.png" )
1048+ except (socket.error, ConnectionError, EOFError, OSError, PyNUT.PyNUTError) as e:
1049+ # Connection error detected — attempt automatic reconnection
1050+ if not self.__reconnect_attempted and self.__parent_class._interface__connected:
1051+ self.__reconnect_attempted = True
1052+ self.__parent_class.gui_status_message( _("Connection lost — attempting to reconnect...") )
1053+ self.__parent_class.gui_status_notification( _("Connection lost"), "warning.png" )
1054+
1055+ # Stop the current timer
1056+ self.__timer.stop()
1057+
1058+ # Close the socket to avoid "Broken pipe" in subsequent calls if any
1059+ if self.__parent_class._interface__ups_handler:
1060+ self.__parent_class._interface__ups_handler.disconnect()
1061+
1062+ # Schedule reconnection attempt after 2 seconds
1063+ QTimer.singleShot(2000, self.__attempt_reconnect)
1064+ elif self.__reconnect_attempted:
1065+ # Reconnection already in progress, just ignore this update
1066+ pass
1067+ else:
1068+ self.__parent_class.gui_status_message( _("Error from '{0}' ({1})").format( ups, sys.exc_info()[1] ) )
1069+ self.__parent_class.gui_status_notification( _("Error from '{0}'\n{1}").format( ups, sys.exc_info()[1] ), "warning.png" )
1070+
1071+ def __attempt_reconnect( self ) :
1072+ """Attempt to reconnect to the UPS after a connection loss."""
1073+ if self.__stop_thread:
1074+ return
1075+
1076+ try:
1077+ # Get connection parameters from the parent class
1078+ host = self.__parent_class._interface__widgets["ups_host_entry"].text()
1079+ port = int(self.__parent_class._interface__widgets["ups_port_entry"].value())
1080+ login = None
1081+ password = None
1082+
1083+ if self.__parent_class._interface__widgets["ups_authentication_check"].isChecked():
1084+ login = self.__parent_class._interface__widgets["ups_authentication_login"].text()
1085+ password = self.__parent_class._interface__widgets["ups_authentication_password"].text()
1086+
1087+ # Teardown old connection handler properly
1088+ old_handler = self.__parent_class._interface__ups_handler
1089+ if old_handler is not None:
1090+ old_handler.disconnect()
1091+ del old_handler
1092+
1093+ # Create a new connection handler
1094+ ups_handler = PyNUT.PyNUTClient(host=host, port=port, login=login, password=password)
1095+
1096+ # Verify the UPS is available
1097+ ups = self.__parent_class._interface__current_ups
1098+ srv_upses = ups_handler.GetUPSList()
1099+
1100+ if ups.encode('ascii') not in srv_upses:
1101+ self.__parent_class.gui_status_message( _("UPS '{0}' not found after reconnection").format(ups) )
1102+ self.__reconnect_attempted = False
1103+ # Restart the timer to continue polling
1104+ self.__timer.start(1000)
1105+ return
1106+
1107+ if not ups_handler.CheckUPSAvailable(ups):
1108+ self.__parent_class.gui_status_message( _("UPS '{0}' is still not reachable").format(ups) )
1109+ self.__reconnect_attempted = False
1110+ self.__timer.start(1000)
1111+ return
1112+
1113+ # Connection successful — update the parent class
1114+ self.__parent_class._interface__ups_handler = ups_handler
1115+ self.__parent_class._interface__connected = True
1116+ self.__reconnect_attempted = False
1117+
1118+ # Refresh UPS data
1119+ commands = ups_handler.GetUPSCommands(ups)
1120+ self.__parent_class._interface__ups_commands = list(commands.keys())
1121+ self.__parent_class._interface__ups_commands.sort()
1122+
1123+ self.__parent_class._interface__ups_vars = ups_handler.GetUPSVars(ups)
1124+ self.__parent_class._interface__ups_rw_vars = ups_handler.GetRWVars(ups)
1125+
1126+ # Update the GUI
1127+ self.__parent_class._interface__gui_update_ups_vars_view()
1128+ self.__parent_class.change_status_icon("on_line", blink=False)
1129+ self.__parent_class.gui_status_message( _("Reconnected to '{0}' on {1}").format(ups, host) )
1130+ self.__parent_class.gui_status_notification( _("Reconnected to UPS"), "on_line.png" )
1131+
1132+ except (socket.error, ConnectionError, EOFError, OSError, PyNUT.PyNUTError) as e:
1133+ self.__parent_class.gui_status_message( _("Reconnection failed: {0}").format(str(e)) )
1134+ self.__reconnect_attempted = False
1135+
1136+ # Restart the timer to continue polling
1137+ self.__timer.start(1000)
10371138
10381139 def stop_thread( self ) :
1039- self.__timer.stop()
1140+ self.__stop_thread = True
1141+ if hasattr(self, '_gui_updater__timer'):
1142+ self.__timer.stop()
10401143
10411144
10421145#-----------------------------------------------------------------------
0 commit comments