main
  1package main
  2
  3import (
  4	"flag"
  5	"fmt"
  6	"io/ioutil"
  7	"log"
  8	"os"
  9	"os/exec"
 10	"path/filepath"
 11	"strconv"
 12	"strings"
 13	"time"
 14)
 15
 16func main() {
 17	// 1. Define command-line flags for configuration
 18	batteryPath := flag.String("battery-path", "/sys/class/power_supply/BAT0", "Path to the battery status directory (e.g., /sys/class/power_supply/BAT0)")
 19	acPath := flag.String("ac-path", "/sys/class/power_supply/AC", "Path to the AC adapter status directory (e.g., /sys/class/power_supply/AC)")
 20	lowThreshold := flag.Int("low-threshold", 40, "Battery percentage threshold for power-saver profile when on battery")
 21	onPowerProfile := flag.String("on-power-profile", "performance", "Power profile to set when on AC power (regardless of charging status)")
 22	onBatteryBalancedProfile := flag.String("on-battery-balanced-profile", "balanced", "Power profile to set when on battery and above low threshold")
 23	onBatteryLowProfile := flag.String("on-battery-low-profile", "power-saver", "Power profile to set when on battery and below low threshold")
 24	enableNotifications := flag.Bool("enable-notifications", true, "Enable desktop notifications using notify-send")
 25	notificationIcon := flag.String("notification-icon", "battery", "Icon name for desktop notifications (e.g., 'battery', 'dialog-information')")
 26	interval := flag.Duration("interval", 10*time.Second, "Interval between battery checks (e.g., 10s, 1m)") // Reverted to polling interval
 27
 28	flag.Parse()
 29
 30	log.Printf("Starting battery monitor with settings:")
 31	log.Printf("  Battery Path: %s", *batteryPath)
 32	log.Printf("  AC Path: %s", *acPath)
 33	log.Printf("  Low Threshold (on battery): %d%%", *lowThreshold)
 34	log.Printf("  On AC Power Profile: %s", *onPowerProfile)
 35	log.Printf("  On Battery Balanced Profile: %s", *onBatteryBalancedProfile)
 36	log.Printf("  On Battery Low Profile: %s", *onBatteryLowProfile)
 37	log.Printf("  Desktop Notifications Enabled: %t", *enableNotifications)
 38	if *enableNotifications {
 39		log.Printf("  Notification Icon: %s", *notificationIcon)
 40	}
 41	log.Printf("  Check Interval: %s", *interval)
 42
 43	// Determine the full paths to relevant files
 44	batteryCapacityFilePath := filepath.Join(*batteryPath, "capacity")
 45	batteryStatusFilePath := filepath.Join(*batteryPath, "status") // e.g., "Charging", "Discharging", "Full"
 46	acOnlineFilePath := filepath.Join(*acPath, "online")           // 0 or 1
 47
 48	// Ensure the necessary paths exist
 49	if _, err := os.Stat(batteryCapacityFilePath); os.IsNotExist(err) {
 50		log.Fatalf("Error: Battery capacity file not found at %s. Please check --battery-path.", batteryCapacityFilePath)
 51	}
 52	if _, err := os.Stat(batteryStatusFilePath); os.IsNotExist(err) {
 53		log.Fatalf("Error: Battery status file not found at %s. Please check --battery-path.", batteryStatusFilePath)
 54	}
 55	if _, err := os.Stat(acOnlineFilePath); os.IsNotExist(err) {
 56		log.Fatalf("Error: AC online file not found at %s. Please check --ac-path.", acOnlineFilePath)
 57	}
 58
 59	currentProfile := "" // To keep track of the currently set profile
 60
 61	// Main monitoring loop using time.Ticker for polling
 62	ticker := time.NewTicker(*interval)
 63	defer ticker.Stop()
 64
 65	// Perform initial check immediately
 66	log.Println("Performing initial battery status check...")
 67	acConnected, batteryStatus, batteryCapacity, err := readSystemStatus(acOnlineFilePath, batteryStatusFilePath, batteryCapacityFilePath)
 68	if err != nil {
 69		log.Printf("Initial check failed: %v. Will retry on next interval.", err)
 70	} else {
 71		currentProfile = applyPowerProfile(acConnected, batteryStatus, batteryCapacity, *lowThreshold, *onPowerProfile, *onBatteryBalancedProfile, *onBatteryLowProfile, currentProfile, *enableNotifications, *notificationIcon)
 72	}
 73
 74	for range ticker.C {
 75		log.Println("Performing scheduled battery status check...")
 76		acConnected, batteryStatus, batteryCapacity, err := readSystemStatus(acOnlineFilePath, batteryStatusFilePath, batteryCapacityFilePath)
 77		if err != nil {
 78			log.Printf("Error reading system status: %v", err)
 79			continue
 80		}
 81		currentProfile = applyPowerProfile(acConnected, batteryStatus, batteryCapacity, *lowThreshold, *onPowerProfile, *onBatteryBalancedProfile, *onBatteryLowProfile, currentProfile, *enableNotifications, *notificationIcon)
 82	}
 83}
 84
 85// readSystemStatus reads the AC online status, battery status, and battery capacity.
 86func readSystemStatus(acOnlinePath, batteryStatusPath, batteryCapacityPath string) (bool, string, int, error) {
 87	// Read AC online status
 88	acOnlineContent, err := ioutil.ReadFile(acOnlinePath)
 89	if err != nil {
 90		return false, "", 0, fmt.Errorf("failed to read AC online file %s: %w", acOnlinePath, err)
 91	}
 92	acOnlineStr := strings.TrimSpace(string(acOnlineContent))
 93	acOnline, err := strconv.Atoi(acOnlineStr)
 94	if err != nil {
 95		return false, "", 0, fmt.Errorf("failed to parse AC online status '%s': %w", acOnlineStr, err)
 96	}
 97	acConnected := acOnline == 1
 98
 99	// Read battery status
100	batteryStatusContent, err := ioutil.ReadFile(batteryStatusPath)
101	if err != nil {
102		return false, "", 0, fmt.Errorf("failed to read battery status file %s: %w", batteryStatusPath, err)
103	}
104	batteryStatus := strings.TrimSpace(string(batteryStatusContent))
105
106	// Read battery capacity
107	batteryCapacityContent, err := ioutil.ReadFile(batteryCapacityPath)
108	if err != nil {
109		return false, "", 0, fmt.Errorf("failed to read battery capacity file %s: %w", batteryCapacityPath, err)
110	}
111	batteryCapacityStr := strings.TrimSpace(string(batteryCapacityContent))
112	batteryCapacity, err := strconv.Atoi(batteryCapacityStr)
113	if err != nil {
114		return false, "", 0, fmt.Errorf("failed to parse battery capacity '%s': %w", batteryCapacityStr, err)
115	}
116	return acConnected, batteryStatus, batteryCapacity, nil
117}
118
119// applyPowerProfile determines and sets the correct power profile and sends a notification.
120// It returns the profile that was actually set (or determined to be set).
121func applyPowerProfile(acConnected bool, batteryStatus string, batteryCapacity int, lowThreshold int, onPowerProfile, onBatteryBalancedProfile, onBatteryLowProfile, currentProfile string, enableNotifications bool, notificationIcon string) string {
122	var newProfile string
123	var notificationMessage string
124
125	log.Printf("Current AC Connected: %t, Battery Status: %s, Capacity: %d%%", acConnected, batteryStatus, batteryCapacity)
126
127	if acConnected {
128		newProfile = onPowerProfile
129		notificationMessage = fmt.Sprintf("Power connected. Switching to %s profile. Battery: %d%% (%s)", newProfile, batteryCapacity, batteryStatus)
130	} else { // On battery
131		if batteryCapacity <= lowThreshold {
132			newProfile = onBatteryLowProfile
133			notificationMessage = fmt.Sprintf("Battery low (%d%%). Switching to %s profile.", batteryCapacity, newProfile)
134		} else {
135			newProfile = onBatteryBalancedProfile
136			notificationMessage = fmt.Sprintf("Running on battery (%d%%). Switching to %s profile.", batteryCapacity, newProfile)
137		}
138	}
139
140	if newProfile != currentProfile {
141		log.Printf("Calculated new profile: %s. Attempting to set.", newProfile)
142		err := setPowerProfile(newProfile)
143		if err != nil {
144			log.Printf("Error setting power profile to %s: %v", newProfile, err)
145			if enableNotifications {
146				sendNotification("Battery Monitor Error", fmt.Sprintf("Failed to set power profile to %s: %v", newProfile, err), "dialog-error")
147			}
148			return currentProfile // If setting failed, we stick to the old profile
149		} else {
150			log.Printf("Successfully set power profile to %s", newProfile)
151			if enableNotifications {
152				sendNotification("Power Profile Changed", notificationMessage, notificationIcon)
153			}
154			return newProfile // Update currentProfile only on success
155		}
156	} else {
157		log.Printf("Power profile already set to %s. No change needed.", currentProfile)
158		return currentProfile // Return the current profile as no change occurred
159	}
160}
161
162// setPowerProfile executes the powerprofilesctl command to set the profile.
163func setPowerProfile(profile string) error {
164	cmd := exec.Command("powerprofilesctl", "set", profile)
165	output, err := cmd.CombinedOutput()
166	if err != nil {
167		return fmt.Errorf("command 'powerprofilesctl set %s' failed: %w\nOutput: %s", profile, err, string(output))
168	}
169	log.Printf("powerprofilesctl output: %s", strings.TrimSpace(string(output)))
170	return nil
171}
172
173// sendNotification executes the notify-send command.
174func sendNotification(summary, body, icon string) {
175	cmd := exec.Command("notify-send", "-i", icon, summary, body)
176	err := cmd.Run()
177	if err != nil {
178		log.Printf("Error sending notification: %v (Is notify-send installed and a notification daemon running?)", err)
179	}
180}