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
346 lines
15 KiB
Python
Executable File
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()
|