Files
beszel/agent/battery/battery.go
T
henrygd 87405c5f10 feat: add multi-battery monitoring
- Report battery data for individual devices
- Select a representative battery for legacy fields and alerts
- Average named battery data independently
- Display multiple batteries in system charts
- Add cross-platform coverage and transport tests
2026-08-16 13:52:29 -04:00

71 lines
1.8 KiB
Go

// Package battery provides battery information for the host and connected devices.
package battery
import (
"errors"
"sort"
"strconv"
"strings"
)
const (
stateUnknown uint8 = iota
stateEmpty
stateFull
stateCharging
stateDischarging
stateIdle
)
// Battery is a readable battery reported by the operating system.
type Battery struct {
Name string
Percent uint8
State uint8
FullChargeCapacity uint64
HasFullChargeCapacity bool
System bool
}
var errNoBatteries = errors.New("no readable batteries")
// normalizeBatteries supplies stable fallback names and disambiguates duplicates.
func normalizeBatteries(batteries []Battery) []Battery {
nameCounts := make(map[string]int, len(batteries))
for i := range batteries {
name := strings.TrimSpace(batteries[i].Name)
if name == "" {
name = "Battery " + strconv.Itoa(i+1)
}
nameCounts[name]++
if nameCounts[name] > 1 {
name += " (" + strconv.Itoa(nameCounts[name]) + ")"
}
batteries[i].Name = name
}
return batteries
}
// Primary returns the representative battery. Reported full-charge capacity wins,
// then system-scoped devices, then name for deterministic ties.
func Primary(batteries []Battery) (Battery, bool) {
if len(batteries) == 0 {
return Battery{}, false
}
ordered := append([]Battery(nil), batteries...)
sort.SliceStable(ordered, func(i, j int) bool {
a, b := ordered[i], ordered[j]
if a.HasFullChargeCapacity != b.HasFullChargeCapacity {
return a.HasFullChargeCapacity
}
if a.HasFullChargeCapacity && a.FullChargeCapacity != b.FullChargeCapacity {
return a.FullChargeCapacity > b.FullChargeCapacity
}
if a.System != b.System {
return a.System
}
return a.Name < b.Name
})
return ordered[0], true
}