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}