Files
battery_management/system-settings-gui.py
root c95362ac35 Initial commit: T14 System Settings GUI and Tray application
Features:
- CPU governor control (performance/powersave)
- Fan level control (L1-L7) with thinkfan integration
- Battery charge threshold management (75-80%, 80-90%, 0-100%)
- Quick charge to 100% option
- Automated battery management (time-based and network-based)
- System tray application with auto-start
- Privilege elevation via pkexec helper script
2025-12-11 12:53:22 +01:00

346 lines
15 KiB
Python
Executable File

#!/usr/bin/env python3
import tkinter as tk
from tkinter import ttk
import subprocess
import threading
class SystemSettingsGUI:
def __init__(self, root):
self.root = root
self.root.title("T14 System Settings")
self.root.geometry("500x600")
self.root.configure(bg='#f0f0f0')
# Title
title = tk.Label(root, text="T14 System Settings", font=("Arial", 16, "bold"), bg='#f0f0f0')
title.pack(pady=10)
# CPU Governor Section
self.create_section(root, "CPU Governor")
self.gov_var = tk.StringVar()
gov_frame = tk.Frame(root, bg='#f0f0f0')
gov_frame.pack(pady=5)
tk.Button(gov_frame, text="Performance", width=15, command=lambda: self.set_governor("performance")).pack(side=tk.LEFT, padx=5)
tk.Button(gov_frame, text="Powersave", width=15, command=lambda: self.set_governor("powersave")).pack(side=tk.LEFT, padx=5)
self.gov_label = tk.Label(root, text="", font=("Arial", 10), bg='#f0f0f0', fg='#0066cc')
self.gov_label.pack()
# Fan Level Section
self.create_section(root, "Fan Level")
fan_frame = tk.Frame(root, bg='#f0f0f0')
fan_frame.pack(pady=5)
for level in range(1, 8):
tk.Button(fan_frame, text=f"L{level}", width=5, command=lambda l=level: self.set_fan_level(l)).pack(side=tk.LEFT, padx=2)
self.fan_label = tk.Label(root, text="", font=("Arial", 10), bg='#f0f0f0', fg='#0066cc')
self.fan_label.pack()
# Battery Thresholds Section
self.create_section(root, "Battery Charging - Manual")
battery_frame = tk.Frame(root, bg='#f0f0f0')
battery_frame.pack(pady=5)
tk.Button(battery_frame, text="Conservative (75-80%)", width=20, command=lambda: self.set_battery(75, 80)).pack(pady=5)
tk.Button(battery_frame, text="Moderate (80-90%)", width=20, command=lambda: self.set_battery(80, 90)).pack(pady=5)
tk.Button(battery_frame, text="Full Charge (0-100%)", width=20, command=lambda: self.set_battery(0, 100)).pack(pady=5)
tk.Button(battery_frame, text="⚡ Charge to 100% NOW", width=20, bg='#ffaa00', command=self.charge_full_now).pack(pady=5)
self.battery_label = tk.Label(root, text="", font=("Arial", 10), bg='#f0f0f0', fg='#0066cc')
self.battery_label.pack()
# Automated Battery Management Section
self.create_section(root, "Automated Battery Management")
auto_frame = tk.Frame(root, bg='#f0f0f0')
auto_frame.pack(pady=5)
tk.Button(auto_frame, text="Setup Time-Based (3:45 PM)", width=25, command=self.setup_time_based).pack(pady=3)
tk.Button(auto_frame, text="Setup Network-Based", width=25, command=self.setup_network_based).pack(pady=3)
self.auto_label = tk.Label(root, text="Not configured", font=("Arial", 9), bg='#f0f0f0', fg='#666666')
self.auto_label.pack()
# Status Section
self.create_section(root, "Current Status")
self.status_label = tk.Label(root, text="Loading...", font=("Arial", 9), bg='#f0f0f0', justify=tk.LEFT)
self.status_label.pack(pady=10)
# Refresh button
tk.Button(root, text="Refresh Status", command=self.refresh_all).pack(pady=10)
# Start refresh thread
self.refresh_all()
def create_section(self, parent, title):
sep = ttk.Separator(parent, orient='horizontal')
sep.pack(fill='x', pady=10)
label = tk.Label(parent, text=title, font=("Arial", 12, "bold"), bg='#f0f0f0')
label.pack()
def run_command(self, cmd):
try:
result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=5)
return result.stdout.strip()
except Exception as e:
return f"Error: {e}"
def set_governor(self, gov):
self.run_command(f"pkexec /usr/local/bin/system-settings-helper.sh cpu-governor {gov}")
self.update_governor_label()
def update_governor_label(self):
gov = self.run_command("cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor")
self.gov_label.config(text=f"Current: {gov.upper()}")
def set_fan_level(self, level):
if level == 2:
# Level 2 = re-enable automatic control via thinkfan
self.run_command("pkexec /usr/local/bin/system-settings-helper.sh fan-start-thinkfan")
self.fan_label.config(text="Fan: Automatic (thinkfan managing)")
else:
# Other levels = manual control, stop thinkfan
self.run_command("pkexec /usr/local/bin/system-settings-helper.sh fan-stop-thinkfan")
self.run_command(f"pkexec /usr/local/bin/system-settings-helper.sh fan-level {level}")
self.update_fan_label()
def update_fan_label(self):
fan_info = self.run_command("grep -E 'speed|level' /proc/acpi/ibm/fan | head -2")
self.fan_label.config(text=fan_info)
def set_battery(self, start, end):
# Get current capacity and status
capacity = int(self.run_command("cat /sys/class/power_supply/BAT0/capacity"))
status = self.run_command("cat /sys/class/power_supply/BAT0/status")
# If currently charging and new end threshold is below current capacity, stop charging first
if status == "Charging" and end < capacity:
# Set end threshold below current to stop charging
self.run_command(f"pkexec /usr/local/bin/system-settings-helper.sh battery-end {capacity - 1}")
import time
time.sleep(0.5)
# Now set the desired thresholds
self.run_command(f"pkexec /usr/local/bin/system-settings-helper.sh battery-start {start}")
self.run_command(f"pkexec /usr/local/bin/system-settings-helper.sh battery-end {end}")
self.update_battery_label()
def update_battery_label(self):
start = self.run_command("cat /sys/class/power_supply/BAT0/charge_control_start_threshold")
end = self.run_command("cat /sys/class/power_supply/BAT0/charge_control_end_threshold")
capacity = self.run_command("cat /sys/class/power_supply/BAT0/capacity")
self.battery_label.config(text=f"Thresholds: {start}-{end}% | Current: {capacity}%")
def charge_full_now(self):
# Set to full range to force charging
self.run_command("pkexec /usr/local/bin/system-settings-helper.sh battery-start 0")
self.run_command("pkexec /usr/local/bin/system-settings-helper.sh battery-end 100")
# Wait a moment and check
import time
time.sleep(0.5)
capacity = self.run_command("cat /sys/class/power_supply/BAT0/capacity")
status = self.run_command("cat /sys/class/power_supply/BAT0/status")
# Visual feedback
self.battery_label.config(text=f"⚡ Charging to 100% | Status: {status} | Current: {capacity}%", fg='#ff6600')
# Refresh after a second to show updated status
self.root.after(2000, self.update_battery_label)
def refresh_all(self):
thread = threading.Thread(target=self._refresh_status)
thread.daemon = True
thread.start()
def _refresh_status(self):
temp = self.run_command("cat /sys/class/hwmon/hwmon5/temp1_input | awk '{print int($1/1000)}'")
gov = self.run_command("cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor")
fan = self.run_command("grep '^speed' /proc/acpi/ibm/fan | awk '{print $NF}'")
capacity = self.run_command("cat /sys/class/power_supply/BAT0/capacity")
status = self.run_command("cat /sys/class/power_supply/BAT0/status")
status_text = f"""
CPU Temperature: {temp}°C
Governor: {gov.upper()}
Fan Speed: {fan} RPM
Battery: {capacity}%
Status: {status}
"""
self.status_label.config(text=status_text)
self.update_governor_label()
self.update_fan_label()
self.update_battery_label()
def setup_time_based(self):
# Create a dialog for time-based setup
dialog = tk.Toplevel(self.root)
dialog.title("Time-Based Battery Management")
dialog.geometry("400x250")
dialog.configure(bg='#f0f0f0')
tk.Label(dialog, text="Time-Based Battery Management", font=("Arial", 12, "bold"), bg='#f0f0f0').pack(pady=10)
tk.Label(dialog, text="At 3:45 PM: Switch to 0-100% charging\nBefore 3:45 PM: Keep 75-80% charging", bg='#f0f0f0').pack(pady=10)
tk.Label(dialog, text="Time threshold (HH:MM):", bg='#f0f0f0').pack()
time_entry = tk.Entry(dialog, width=10)
time_entry.insert(0, "15:45")
time_entry.pack(pady=5)
def confirm_time_based():
threshold_time = time_entry.get()
self.install_time_based_management(threshold_time)
dialog.destroy()
tk.Button(dialog, text="Install", command=confirm_time_based).pack(pady=10)
def setup_network_based(self):
# Create a dialog for network-based setup
dialog = tk.Toplevel(self.root)
dialog.title("Network-Based Battery Management")
dialog.geometry("450x250")
dialog.configure(bg='#f0f0f0')
tk.Label(dialog, text="Network-Based Battery Management", font=("Arial", 12, "bold"), bg='#f0f0f0').pack(pady=10)
tk.Label(dialog, text="Logic:\n• At office before 3:45 PM → 75-80% (conservative)\n• At office after 3:45 PM → 0-100% (full charge)\n• At home (not on office WiFi) → 75-80% (conservative)",
justify=tk.LEFT, bg='#f0f0f0').pack(pady=10)
tk.Label(dialog, text="Office WiFi SSID:", bg='#f0f0f0').pack()
office_entry = tk.Entry(dialog, width=30)
office_entry.pack(pady=5)
tk.Label(dialog, text="Time threshold (HH:MM):", bg='#f0f0f0').pack()
time_entry = tk.Entry(dialog, width=10)
time_entry.insert(0, "15:45")
time_entry.pack(pady=5)
def confirm_network_based():
office_ssid = office_entry.get()
threshold_time = time_entry.get()
if office_ssid:
self.install_network_based_management(office_ssid, threshold_time)
dialog.destroy()
tk.Button(dialog, text="Install", command=confirm_network_based).pack(pady=10)
def install_time_based_management(self, threshold_time):
# Create systemd timer for time-based switching
script_content = f"""#!/bin/bash
# Switch to full charging at {threshold_time}
echo 0 > /sys/class/power_supply/BAT0/charge_control_start_threshold
echo 100 > /sys/class/power_supply/BAT0/charge_control_end_threshold
echo "[$(date)] Switched to 0-100% charging (time-based)" >> /var/log/battery-thresholds.log
"""
self.run_command(f"sudo tee /usr/local/bin/battery-charge-full.sh > /dev/null << 'EOF'\n{script_content}EOF")
self.run_command("sudo chmod +x /usr/local/bin/battery-charge-full.sh")
# Create systemd timer
timer_unit = f"""[Unit]
Description=Switch battery to full charge at {threshold_time}
After=network.target
[Timer]
OnCalendar=*-*-* {threshold_time}:00
Persistent=true
[Install]
WantedBy=timers.target
"""
self.run_command(f"sudo tee /etc/systemd/system/battery-charge-full.timer > /dev/null << 'EOF'\n{timer_unit}EOF")
service_unit = """[Unit]
Description=Battery full charge service
After=network.target
[Service]
Type=oneshot
ExecStart=/usr/local/bin/battery-charge-full.sh
"""
self.run_command(f"sudo tee /etc/systemd/system/battery-charge-full.service > /dev/null << 'EOF'\n{service_unit}EOF")
self.run_command("sudo systemctl daemon-reload")
self.run_command("sudo systemctl enable battery-charge-full.timer")
self.run_command("sudo systemctl start battery-charge-full.timer")
self.auto_label.config(text=f"✓ Time-based enabled (switch at {threshold_time})", fg='#009900')
def install_network_based_management(self, office_ssid, threshold_time):
# Create network monitoring script
script_content = f"""#!/bin/bash
# Network-based battery management
OFFICE_SSID="{office_ssid}"
THRESHOLD_TIME="{threshold_time}"
CURRENT_SSID=$(nmcli -t -f active,ssid dev wifi | grep '^yes' | cut -d: -f2)
CURRENT_TIME=$(date +%H:%M)
if [ "$CURRENT_SSID" = "$OFFICE_SSID" ]; then
# At office - check time
if [ "$CURRENT_TIME" \>= "$THRESHOLD_TIME" ]; then
# After threshold time - charge to full before leaving
echo 0 > /sys/class/power_supply/BAT0/charge_control_start_threshold
echo 100 > /sys/class/power_supply/BAT0/charge_control_end_threshold
echo "[$(date)] Office WiFi + after $THRESHOLD_TIME - 0-100% charging (full before leaving)" >> /var/log/battery-thresholds.log
else
# Before threshold time - conservative charging
echo 75 > /sys/class/power_supply/BAT0/charge_control_start_threshold
echo 80 > /sys/class/power_supply/BAT0/charge_control_end_threshold
echo "[$(date)] Office WiFi + before $THRESHOLD_TIME - 75-80% charging (conservative)" >> /var/log/battery-thresholds.log
fi
else
# Not on office WiFi - assume at home, conservative charging
echo 75 > /sys/class/power_supply/BAT0/charge_control_start_threshold
echo 80 > /sys/class/power_supply/BAT0/charge_control_end_threshold
echo "[$(date)] Not on office WiFi (at home) - 75-80% charging (conservative)" >> /var/log/battery-thresholds.log
fi
"""
self.run_command(f"sudo tee /usr/local/bin/battery-network-mgmt.sh > /dev/null << 'EOFSCRIPT'\n{script_content}EOFSCRIPT")
self.run_command("sudo chmod +x /usr/local/bin/battery-network-mgmt.sh")
# Create dispatcher script for NetworkManager
self.run_command(f"sudo tee /etc/NetworkManager/dispatcher.d/99-battery-management > /dev/null << 'EOF'\n#!/bin/bash\n/usr/local/bin/battery-network-mgmt.sh\nEOF")
self.run_command("sudo chmod +x /etc/NetworkManager/dispatcher.d/99-battery-management")
# Also create systemd timer to check every 5 minutes as backup
timer_content = """[Unit]
Description=Battery Network Management Timer
After=network.target
[Timer]
OnBootSec=2min
OnUnitActiveSec=5min
Persistent=true
[Install]
WantedBy=timers.target
"""
self.run_command(f"sudo tee /etc/systemd/system/battery-network-check.timer > /dev/null << 'EOF'\n{timer_content}EOF")
service_content = """[Unit]
Description=Battery Network Management Check
After=network.target
[Service]
Type=oneshot
ExecStart=/usr/local/bin/battery-network-mgmt.sh
"""
self.run_command(f"sudo tee /etc/systemd/system/battery-network-check.service > /dev/null << 'EOF'\n{service_content}EOF")
self.run_command("sudo systemctl daemon-reload")
self.run_command("sudo systemctl enable battery-network-check.timer")
self.run_command("sudo systemctl start battery-network-check.timer")
# Run once immediately
self.run_command("sudo /usr/local/bin/battery-network-mgmt.sh")
self.auto_label.config(text=f"✓ Network-based enabled (Office: {office_ssid}, at {threshold_time})", fg='#009900')
if __name__ == "__main__":
root = tk.Tk()
app = SystemSettingsGUI(root)
root.mainloop()