Add low-battery warning and auto-shutdown to the tray icon

The oneUpPower kernel module powered the system off at low SOC, but the
icon now reads the battery over I2C and no longer relies on that module.
Restore the safety net in the icon itself.

While running on battery, warn via a tray notification at <=10% and run
`systemctl poweroff` at <=5%. systemctl poweroff is authorised for the
active desktop session without sudo (logind CanPowerOff returns "yes").
State resets when AC is reconnected.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joachim Hummel
2026-05-16 23:03:54 +02:00
parent 19f1e2962e
commit 2e65dadfa9

View File

@@ -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()