Commit 0052583a4859

Vincent Demeester <vincent@sbr.pm>
2025-06-18 22:24:01
tools/battery-monitor: switch to polling…
Not the best but for `/sys`, we can't really too much. The other "guess" would be to use upower. Signed-off-by: Vincent Demeester <vincent@sbr.pm>
1 parent 45c02e5
Changed files (1)
tools
battery-monitor
tools/battery-monitor/main.go
@@ -10,24 +10,26 @@ import (
 	"path/filepath"
 	"strconv"
 	"strings"
-
-	"github.com/fsnotify/fsnotify"
+	"time"
 )
 
 func main() {
 	// 1. Define command-line flags for configuration
 	batteryPath := flag.String("battery-path", "/sys/class/power_supply/BAT0", "Path to the battery status directory (e.g., /sys/class/power_supply/BAT0)")
+	acPath := flag.String("ac-path", "/sys/class/power_supply/AC", "Path to the AC adapter status directory (e.g., /sys/class/power_supply/AC)")
 	lowThreshold := flag.Int("low-threshold", 40, "Battery percentage threshold for power-saver profile when on battery")
-	onPowerProfile := flag.String("on-power-profile", "performance", "Power profile to set when on AC power")
+	onPowerProfile := flag.String("on-power-profile", "performance", "Power profile to set when on AC power (regardless of charging status)")
 	onBatteryBalancedProfile := flag.String("on-battery-balanced-profile", "balanced", "Power profile to set when on battery and above low threshold")
 	onBatteryLowProfile := flag.String("on-battery-low-profile", "power-saver", "Power profile to set when on battery and below low threshold")
 	enableNotifications := flag.Bool("enable-notifications", true, "Enable desktop notifications using notify-send")
 	notificationIcon := flag.String("notification-icon", "battery", "Icon name for desktop notifications (e.g., 'battery', 'dialog-information')")
+	interval := flag.Duration("interval", 10*time.Second, "Interval between battery checks (e.g., 10s, 1m)") // Reverted to polling interval
 
 	flag.Parse()
 
 	log.Printf("Starting battery monitor with settings:")
 	log.Printf("  Battery Path: %s", *batteryPath)
+	log.Printf("  AC Path: %s", *acPath)
 	log.Printf("  Low Threshold (on battery): %d%%", *lowThreshold)
 	log.Printf("  On AC Power Profile: %s", *onPowerProfile)
 	log.Printf("  On Battery Balanced Profile: %s", *onBatteryBalancedProfile)
@@ -36,126 +38,107 @@ func main() {
 	if *enableNotifications {
 		log.Printf("  Notification Icon: %s", *notificationIcon)
 	}
+	log.Printf("  Check Interval: %s", *interval)
 
 	// Determine the full paths to relevant files
-	capacityFilePath := filepath.Join(*batteryPath, "capacity")
-	statusFilePath := filepath.Join(*batteryPath, "status") // Typically "Charging" or "Discharging"
+	batteryCapacityFilePath := filepath.Join(*batteryPath, "capacity")
+	batteryStatusFilePath := filepath.Join(*batteryPath, "status") // e.g., "Charging", "Discharging", "Full"
+	acOnlineFilePath := filepath.Join(*acPath, "online")           // 0 or 1
 
-	// Ensure the battery paths exist
-	if _, err := os.Stat(capacityFilePath); os.IsNotExist(err) {
-		log.Fatalf("Error: Battery capacity file not found at %s. Please check --battery-path.", capacityFilePath)
+	// Ensure the necessary paths exist
+	if _, err := os.Stat(batteryCapacityFilePath); os.IsNotExist(err) {
+		log.Fatalf("Error: Battery capacity file not found at %s. Please check --battery-path.", batteryCapacityFilePath)
 	}
-	if _, err := os.Stat(statusFilePath); os.IsNotExist(err) {
-		log.Fatalf("Error: Battery status file not found at %s. Please check --battery-path.", statusFilePath)
+	if _, err := os.Stat(batteryStatusFilePath); os.IsNotExist(err) {
+		log.Fatalf("Error: Battery status file not found at %s. Please check --battery-path.", batteryStatusFilePath)
+	}
+	if _, err := os.Stat(acOnlineFilePath); os.IsNotExist(err) {
+		log.Fatalf("Error: AC online file not found at %s. Please check --ac-path.", acOnlineFilePath)
 	}
 
 	currentProfile := "" // To keep track of the currently set profile
 
-	// Initial check on startup
+	// Main monitoring loop using time.Ticker for polling
+	ticker := time.NewTicker(*interval)
+	defer ticker.Stop()
+
+	// Perform initial check immediately
 	log.Println("Performing initial battery status check...")
-	status, capacity, err := readBatteryStatusAndCapacity(statusFilePath, capacityFilePath)
+	acConnected, batteryStatus, batteryCapacity, err := readSystemStatus(acOnlineFilePath, batteryStatusFilePath, batteryCapacityFilePath)
 	if err != nil {
-		log.Printf("Initial check failed: %v. Retrying on file change.", err)
+		log.Printf("Initial check failed: %v. Will retry on next interval.", err)
 	} else {
-		// Pass notification settings to the apply function
-		currentProfile = applyPowerProfile(status, capacity, *lowThreshold, *onPowerProfile, *onBatteryBalancedProfile, *onBatteryLowProfile, currentProfile, *enableNotifications, *notificationIcon)
+		currentProfile = applyPowerProfile(acConnected, batteryStatus, batteryCapacity, *lowThreshold, *onPowerProfile, *onBatteryBalancedProfile, *onBatteryLowProfile, currentProfile, *enableNotifications, *notificationIcon)
 	}
 
-	// Create a new watcher.
-	watcher, err := fsnotify.NewWatcher()
-	if err != nil {
-		log.Fatal("Error creating watcher:", err)
-	}
-	defer watcher.Close()
-
-	done := make(chan bool)
-
-	go func() {
-		for {
-			select {
-			case event, ok := <-watcher.Events:
-				if !ok {
-					return
-				}
-				if event.Op&fsnotify.Write == fsnotify.Write {
-					log.Printf("File modified: %s - Event: %s", event.Name, event.Op.String())
-
-					// Read status and capacity on change
-					status, capacity, err := readBatteryStatusAndCapacity(statusFilePath, capacityFilePath)
-					if err != nil {
-						log.Printf("Error reading battery status/capacity: %v", err)
-						continue
-					}
-					// Pass notification settings to the apply function
-					currentProfile = applyPowerProfile(status, capacity, *lowThreshold, *onPowerProfile, *onBatteryBalancedProfile, *onBatteryLowProfile, currentProfile, *enableNotifications, *notificationIcon)
-				}
-			case err, ok := <-watcher.Errors:
-				if !ok {
-					return
-				}
-				log.Println("Error from watcher:", err)
-			}
+	for range ticker.C {
+		log.Println("Performing scheduled battery status check...")
+		acConnected, batteryStatus, batteryCapacity, err := readSystemStatus(acOnlineFilePath, batteryStatusFilePath, batteryCapacityFilePath)
+		if err != nil {
+			log.Printf("Error reading system status: %v", err)
+			continue
 		}
-	}()
-
-	err = watcher.Add(*batteryPath)
-	if err != nil {
-		log.Fatalf("Error adding %s to watcher: %v", *batteryPath, err)
+		currentProfile = applyPowerProfile(acConnected, batteryStatus, batteryCapacity, *lowThreshold, *onPowerProfile, *onBatteryBalancedProfile, *onBatteryLowProfile, currentProfile, *enableNotifications, *notificationIcon)
 	}
-	log.Printf("Watching %s for changes...", *batteryPath)
-
-	<-done // Keep the main goroutine alive
 }
 
-// readBatteryStatusAndCapacity reads the battery status (Charging/Discharging) and percentage.
-func readBatteryStatusAndCapacity(statusPath, capacityPath string) (string, int, error) {
-	// Read status
-	statusContent, err := ioutil.ReadFile(statusPath)
+// readSystemStatus reads the AC online status, battery status, and battery capacity.
+func readSystemStatus(acOnlinePath, batteryStatusPath, batteryCapacityPath string) (bool, string, int, error) {
+	// Read AC online status
+	acOnlineContent, err := ioutil.ReadFile(acOnlinePath)
 	if err != nil {
-		return "", 0, fmt.Errorf("failed to read battery status file %s: %w", statusPath, err)
+		return false, "", 0, fmt.Errorf("failed to read AC online file %s: %w", acOnlinePath, err)
 	}
-	status := strings.TrimSpace(string(statusContent))
+	acOnlineStr := strings.TrimSpace(string(acOnlineContent))
+	acOnline, err := strconv.Atoi(acOnlineStr)
+	if err != nil {
+		return false, "", 0, fmt.Errorf("failed to parse AC online status '%s': %w", acOnlineStr, err)
+	}
+	acConnected := acOnline == 1
 
-	// Read capacity
-	capacityContent, err := ioutil.ReadFile(capacityPath)
+	// Read battery status
+	batteryStatusContent, err := ioutil.ReadFile(batteryStatusPath)
 	if err != nil {
-		return "", 0, fmt.Errorf("failed to read battery capacity file %s: %w", capacityPath, err)
+		return false, "", 0, fmt.Errorf("failed to read battery status file %s: %w", batteryStatusPath, err)
 	}
-	capacityStr := strings.TrimSpace(string(capacityContent))
-	capacity, err := strconv.Atoi(capacityStr)
+	batteryStatus := strings.TrimSpace(string(batteryStatusContent))
+
+	// Read battery capacity
+	batteryCapacityContent, err := ioutil.ReadFile(batteryCapacityPath)
 	if err != nil {
-		return "", 0, fmt.Errorf("failed to parse battery capacity '%s': %w", capacityStr, err)
+		return false, "", 0, fmt.Errorf("failed to read battery capacity file %s: %w", batteryCapacityPath, err)
 	}
-	return status, capacity, nil
+	batteryCapacityStr := strings.TrimSpace(string(batteryCapacityContent))
+	batteryCapacity, err := strconv.Atoi(batteryCapacityStr)
+	if err != nil {
+		return false, "", 0, fmt.Errorf("failed to parse battery capacity '%s': %w", batteryCapacityStr, err)
+	}
+	return acConnected, batteryStatus, batteryCapacity, nil
 }
 
 // applyPowerProfile determines and sets the correct power profile and sends a notification.
 // It returns the profile that was actually set (or determined to be set).
-func applyPowerProfile(status string, capacity int, lowThreshold int, onPowerProfile, onBatteryBalancedProfile, onBatteryLowProfile, currentProfile string, enableNotifications bool, notificationIcon string) string {
+func applyPowerProfile(acConnected bool, batteryStatus string, batteryCapacity int, lowThreshold int, onPowerProfile, onBatteryBalancedProfile, onBatteryLowProfile, currentProfile string, enableNotifications bool, notificationIcon string) string {
 	var newProfile string
 	var notificationMessage string
 
-	log.Printf("Current Status: %s, Capacity: %d%%", status, capacity)
+	log.Printf("Current AC Connected: %t, Battery Status: %s, Capacity: %d%%", acConnected, batteryStatus, batteryCapacity)
 
-	if status == "Charging" || status == "Full" {
+	if acConnected {
 		newProfile = onPowerProfile
-		notificationMessage = fmt.Sprintf("Power connected. Switching to %s profile.", newProfile)
-	} else if status == "Discharging" {
-		if capacity <= lowThreshold {
+		notificationMessage = fmt.Sprintf("Power connected. Switching to %s profile. Battery: %d%% (%s)", newProfile, batteryCapacity, batteryStatus)
+	} else { // On battery
+		if batteryCapacity <= lowThreshold {
 			newProfile = onBatteryLowProfile
-			notificationMessage = fmt.Sprintf("Battery low (%d%%). Switching to %s profile.", capacity, newProfile)
+			notificationMessage = fmt.Sprintf("Battery low (%d%%). Switching to %s profile.", batteryCapacity, newProfile)
 		} else {
 			newProfile = onBatteryBalancedProfile
-			notificationMessage = fmt.Sprintf("Battery on power (%d%%). Switching to %s profile.", capacity, newProfile)
+			notificationMessage = fmt.Sprintf("Running on battery (%d%%). Switching to %s profile.", batteryCapacity, newProfile)
 		}
-	} else {
-		log.Printf("Unknown battery status: %s. Defaulting to balanced profile.", status)
-		newProfile = onBatteryBalancedProfile // Fallback
-		notificationMessage = fmt.Sprintf("Unknown battery status '%s'. Defaulting to %s profile.", status, newProfile)
 	}
 
 	if newProfile != currentProfile {
-		log.Printf("Calculated new profile: %s (Current Status: %s, Capacity: %d%%). Attempting to set.", newProfile, status, capacity)
+		log.Printf("Calculated new profile: %s. Attempting to set.", newProfile)
 		err := setPowerProfile(newProfile)
 		if err != nil {
 			log.Printf("Error setting power profile to %s: %v", newProfile, err)