diff --git a/monitor/batteryIcon.py b/monitor/batteryIcon.py index 089892b..63d7f91 100755 --- a/monitor/batteryIcon.py +++ b/monitor/batteryIcon.py @@ -7,10 +7,14 @@ management IC over I2C (bus 1, address 0x64) -- the same registers the oneUpPower kernel module uses. This means the icon works for an ordinary user (member of the ``i2c`` group) without sudo and without the kernel module being loaded. Updates every 5 seconds. + +When running on battery it also warns on low charge and powers the system +off when critically low, mirroring the oneUpPower kernel module. """ import sys import time +import subprocess import smbus2 from PyQt5.QtWidgets import QApplication, QSystemTrayIcon, QMenu, QAction @@ -24,6 +28,10 @@ SOC_REG = 0x04 # state of charge, percent CURRENT_HIGH_REG = 0x0E # bit 7 set => running on battery (not plugged in) UPDATE_INTERVAL_MS = 5000 +# Auto-shutdown while running on battery (mirrors the oneUpPower module). +SHUTDOWN_SOC = 5 # power off at or below this percentage +WARN_SOC = 10 # warn the user at or below this percentage + def read_battery(bus): """Return (percent, plugged_in) read over I2C, or (None, None) on error. @@ -115,6 +123,8 @@ class BatteryTray: time.sleep(2) self.bus = smbus2.SMBus(I2C_BUS) + self.shutdown_triggered = False + self.warned = False self.tray = QSystemTrayIcon() # Context menu @@ -155,6 +165,8 @@ class BatteryTray: tooltip = f"Battery: {percent}% ({state})" self.tray.setToolTip(tooltip) self.status_action.setText(tooltip) + + self.check_power(percent, plugged_in) else: # I2C read failed - IC not reachable icon = make_battery_icon(0, False) @@ -162,6 +174,35 @@ class BatteryTray: self.tray.setToolTip("Battery: N/A (I2C read failed)") self.status_action.setText("Battery: N/A (I2C read failed)") + def check_power(self, percent, plugged_in): + """Warn on low battery and power off when critically low. + + Only acts while running on battery. `systemctl poweroff` works for + the active desktop session without sudo. + """ + if plugged_in: + self.warned = False + return + + if percent <= SHUTDOWN_SOC and not self.shutdown_triggered: + self.shutdown_triggered = True + self.tray.showMessage( + "Battery critical", + f"Battery at {percent}% - shutting down now.", + QSystemTrayIcon.Critical, 10000) + try: + subprocess.run(["systemctl", "poweroff"], + check=True, timeout=10) + except (subprocess.SubprocessError, OSError) as e: + print(f"poweroff failed: {e}", file=sys.stderr) + self.shutdown_triggered = False # allow retry next cycle + elif percent <= WARN_SOC and not self.warned: + self.warned = True + self.tray.showMessage( + "Battery low", + f"Battery at {percent}% - save your work.", + QSystemTrayIcon.Warning, 10000) + def run(self): return self.app.exec()