agent: refactor new battery package (#1872)

This commit is contained in:
henrygd
2026-04-02 19:55:48 -04:00
committed by hank
parent 80b73c7faf
commit d656036d3b
7 changed files with 33 additions and 84 deletions

View File

@@ -1,14 +1,11 @@
//go:build !freebsd && !darwin && !linux && !windows
// Package battery provides functions to check if the system has a battery and to get the battery stats. // Package battery provides functions to check if the system has a battery and to get the battery stats.
package battery package battery
import "errors" const (
stateUnknown uint8 = iota
func HasReadableBattery() bool { stateEmpty
return false stateFull
} stateCharging
stateDischarging
func GetBatteryStats() (uint8, uint8, error) { stateIdle
return 0, 0, errors.ErrUnsupported )
}

View File

@@ -7,19 +7,11 @@ import (
"log/slog" "log/slog"
"math" "math"
"os/exec" "os/exec"
"sync"
"howett.net/plist" "howett.net/plist"
) )
const (
stateUnknown uint8 = 0
stateEmpty uint8 = 1
stateFull uint8 = 2
stateCharging uint8 = 3
stateDischarging uint8 = 4
stateIdle uint8 = 5
)
type macBattery struct { type macBattery struct {
CurrentCapacity int `plist:"CurrentCapacity"` CurrentCapacity int `plist:"CurrentCapacity"`
MaxCapacity int `plist:"MaxCapacity"` MaxCapacity int `plist:"MaxCapacity"`
@@ -28,11 +20,6 @@ type macBattery struct {
ExternalConnected bool `plist:"ExternalConnected"` ExternalConnected bool `plist:"ExternalConnected"`
} }
var (
systemHasBattery = false
haveCheckedBattery = false
)
func readMacBatteries() ([]macBattery, error) { func readMacBatteries() ([]macBattery, error) {
out, err := exec.Command("ioreg", "-n", "AppleSmartBattery", "-r", "-a").Output() out, err := exec.Command("ioreg", "-n", "AppleSmartBattery", "-r", "-a").Output()
if err != nil { if err != nil {
@@ -49,11 +36,8 @@ func readMacBatteries() ([]macBattery, error) {
} }
// HasReadableBattery checks if the system has a battery and returns true if it does. // HasReadableBattery checks if the system has a battery and returns true if it does.
func HasReadableBattery() bool { var HasReadableBattery = sync.OnceValue(func() bool {
if haveCheckedBattery { systemHasBattery := false
return systemHasBattery
}
haveCheckedBattery = true
batteries, err := readMacBatteries() batteries, err := readMacBatteries()
for _, bat := range batteries { for _, bat := range batteries {
if bat.MaxCapacity > 0 { if bat.MaxCapacity > 0 {
@@ -65,7 +49,7 @@ func HasReadableBattery() bool {
slog.Debug("No battery found", "err", err) slog.Debug("No battery found", "err", err)
} }
return systemHasBattery return systemHasBattery
} })
// GetBatteryStats returns the current battery percent and charge state. // GetBatteryStats returns the current battery percent and charge state.
// Uses CurrentCapacity/MaxCapacity to match the value macOS displays. // Uses CurrentCapacity/MaxCapacity to match the value macOS displays.

View File

@@ -9,27 +9,14 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"strconv" "strconv"
"sync"
"github.com/henrygd/beszel/agent/utils" "github.com/henrygd/beszel/agent/utils"
) )
const (
stateUnknown uint8 = 0
stateEmpty uint8 = 1
stateFull uint8 = 2
stateCharging uint8 = 3
stateDischarging uint8 = 4
stateIdle uint8 = 5
)
const sysfsPowerSupply = "/sys/class/power_supply" const sysfsPowerSupply = "/sys/class/power_supply"
var ( var getBatteryPaths = sync.OnceValues(func() ([]string, error) {
systemHasBattery = false
haveCheckedBattery = false
)
func getBatteryPaths() ([]string, error) {
entries, err := os.ReadDir(sysfsPowerSupply) entries, err := os.ReadDir(sysfsPowerSupply)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -42,7 +29,7 @@ func getBatteryPaths() ([]string, error) {
} }
} }
return paths, nil return paths, nil
} })
func parseSysfsState(status string) uint8 { func parseSysfsState(status string) uint8 {
switch status { switch status {
@@ -62,11 +49,8 @@ func parseSysfsState(status string) uint8 {
} }
// HasReadableBattery checks if the system has a battery and returns true if it does. // HasReadableBattery checks if the system has a battery and returns true if it does.
func HasReadableBattery() bool { var HasReadableBattery = sync.OnceValue(func() bool {
if haveCheckedBattery { systemHasBattery := false
return systemHasBattery
}
haveCheckedBattery = true
paths, err := getBatteryPaths() paths, err := getBatteryPaths()
for _, path := range paths { for _, path := range paths {
if _, ok := utils.ReadStringFileOK(filepath.Join(path, "capacity")); ok { if _, ok := utils.ReadStringFileOK(filepath.Join(path, "capacity")); ok {
@@ -78,7 +62,7 @@ func HasReadableBattery() bool {
slog.Debug("No battery found", "err", err) slog.Debug("No battery found", "err", err)
} }
return systemHasBattery return systemHasBattery
} })
// GetBatteryStats returns the current battery percent and charge state. // GetBatteryStats returns the current battery percent and charge state.
// Reads /sys/class/power_supply/*/capacity directly so the kernel-reported // Reads /sys/class/power_supply/*/capacity directly so the kernel-reported

View File

@@ -1,4 +1,4 @@
//go:build freebsd //go:build !darwin && !linux && !windows
package battery package battery

View File

@@ -6,21 +6,13 @@ import (
"errors" "errors"
"log/slog" "log/slog"
"math" "math"
"sync"
"syscall" "syscall"
"unsafe" "unsafe"
"golang.org/x/sys/windows" "golang.org/x/sys/windows"
) )
const (
stateUnknown uint8 = 0
stateEmpty uint8 = 1
stateFull uint8 = 2
stateCharging uint8 = 3
stateDischarging uint8 = 4
stateIdle uint8 = 5
)
type batteryQueryInformation struct { type batteryQueryInformation struct {
BatteryTag uint32 BatteryTag uint32
InformationLevel int32 InformationLevel int32
@@ -77,15 +69,16 @@ var guidDeviceBattery = winGUID{
} }
var ( var (
setupapi = &windows.LazyDLL{Name: "setupapi.dll", System: true} setupapi = &windows.LazyDLL{Name: "setupapi.dll", System: true}
setupDiGetClassDevsW = setupapi.NewProc("SetupDiGetClassDevsW") setupDiGetClassDevsW = setupapi.NewProc("SetupDiGetClassDevsW")
setupDiEnumDeviceInterfaces = setupapi.NewProc("SetupDiEnumDeviceInterfaces") setupDiEnumDeviceInterfaces = setupapi.NewProc("SetupDiEnumDeviceInterfaces")
setupDiGetDeviceInterfaceDetailW = setupapi.NewProc("SetupDiGetDeviceInterfaceDetailW") setupDiGetDeviceInterfaceDetailW = setupapi.NewProc("SetupDiGetDeviceInterfaceDetailW")
setupDiDestroyDeviceInfoList = setupapi.NewProc("SetupDiDestroyDeviceInfoList") setupDiDestroyDeviceInfoList = setupapi.NewProc("SetupDiDestroyDeviceInfoList")
) )
func setupDiSetup(proc *windows.LazyProc, nargs, a1, a2, a3, a4, a5, a6 uintptr) (uintptr, error) { func setupDiSetup(proc *windows.LazyProc, nargs, a1, a2, a3, a4, a5, a6 uintptr) (uintptr, error) {
r1, _, errno := syscall.Syscall6(proc.Addr(), nargs, a1, a2, a3, a4, a5, a6) _ = nargs
r1, _, errno := syscall.SyscallN(proc.Addr(), a1, a2, a3, a4, a5, a6)
if windows.Handle(r1) == windows.InvalidHandle { if windows.Handle(r1) == windows.InvalidHandle {
if errno != 0 { if errno != 0 {
return 0, error(errno) return 0, error(errno)
@@ -96,7 +89,8 @@ func setupDiSetup(proc *windows.LazyProc, nargs, a1, a2, a3, a4, a5, a6 uintptr)
} }
func setupDiCall(proc *windows.LazyProc, nargs, a1, a2, a3, a4, a5, a6 uintptr) syscall.Errno { func setupDiCall(proc *windows.LazyProc, nargs, a1, a2, a3, a4, a5, a6 uintptr) syscall.Errno {
r1, _, errno := syscall.Syscall6(proc.Addr(), nargs, a1, a2, a3, a4, a5, a6) _ = nargs
r1, _, errno := syscall.SyscallN(proc.Addr(), a1, a2, a3, a4, a5, a6)
if r1 == 0 { if r1 == 0 {
if errno != 0 { if errno != 0 {
return errno return errno
@@ -137,7 +131,7 @@ func winBatteryGet(idx int) (full, current uint32, state uint8, err error) {
if err != nil { if err != nil {
return 0, 0, stateUnknown, err return 0, 0, stateUnknown, err
} }
defer syscall.Syscall(setupDiDestroyDeviceInfoList.Addr(), 1, hdev, 0, 0) defer syscall.SyscallN(setupDiDestroyDeviceInfoList.Addr(), hdev)
var did spDeviceInterfaceData var did spDeviceInterfaceData
did.cbSize = uint32(unsafe.Sizeof(did)) did.cbSize = uint32(unsafe.Sizeof(did))
@@ -256,17 +250,9 @@ func winBatteryGet(idx int) (full, current uint32, state uint8, err error) {
return bi.FullChargedCapacity, bs.Capacity, readWinBatteryState(bs.PowerState), nil return bi.FullChargedCapacity, bs.Capacity, readWinBatteryState(bs.PowerState), nil
} }
var (
systemHasBattery = false
haveCheckedBattery = false
)
// HasReadableBattery checks if the system has a battery and returns true if it does. // HasReadableBattery checks if the system has a battery and returns true if it does.
func HasReadableBattery() bool { var HasReadableBattery = sync.OnceValue(func() bool {
if haveCheckedBattery { systemHasBattery := false
return systemHasBattery
}
haveCheckedBattery = true
full, _, _, err := winBatteryGet(0) full, _, _, err := winBatteryGet(0)
if err == nil && full > 0 { if err == nil && full > 0 {
systemHasBattery = true systemHasBattery = true
@@ -275,7 +261,7 @@ func HasReadableBattery() bool {
slog.Debug("No battery found", "err", err) slog.Debug("No battery found", "err", err)
} }
return systemHasBattery return systemHasBattery
} })
// GetBatteryStats returns the current battery percent and charge state. // GetBatteryStats returns the current battery percent and charge state.
func GetBatteryStats() (batteryPercent uint8, batteryState uint8, err error) { func GetBatteryStats() (batteryPercent uint8, batteryState uint8, err error) {

2
go.mod
View File

@@ -5,7 +5,7 @@ go 1.26.1
require ( require (
github.com/blang/semver v3.5.1+incompatible github.com/blang/semver v3.5.1+incompatible
github.com/coreos/go-systemd/v22 v22.7.0 github.com/coreos/go-systemd/v22 v22.7.0
github.com/ebitengine/purego v0.10.0 github.com/ebitengine/purego v0.10.0
github.com/fxamacker/cbor/v2 v2.9.0 github.com/fxamacker/cbor/v2 v2.9.0
github.com/gliderlabs/ssh v0.3.8 github.com/gliderlabs/ssh v0.3.8
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0

2
go.sum
View File

@@ -17,8 +17,6 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c= github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
github.com/distatus/battery v0.11.0 h1:KJk89gz90Iq/wJtbjjM9yUzBXV+ASV/EG2WOOL7N8lc=
github.com/distatus/battery v0.11.0/go.mod h1:KmVkE8A8hpIX4T78QRdMktYpEp35QfOL8A8dwZBxq2k=
github.com/domodwyer/mailyak/v3 v3.6.2 h1:x3tGMsyFhTCaxp6ycgR0FE/bu5QiNp+hetUuCOBXMn8= github.com/domodwyer/mailyak/v3 v3.6.2 h1:x3tGMsyFhTCaxp6ycgR0FE/bu5QiNp+hetUuCOBXMn8=
github.com/domodwyer/mailyak/v3 v3.6.2/go.mod h1:lOm/u9CyCVWHeaAmHIdF4RiKVxKUT/H5XX10lIKAL6c= github.com/domodwyer/mailyak/v3 v3.6.2/go.mod h1:lOm/u9CyCVWHeaAmHIdF4RiKVxKUT/H5XX10lIKAL6c=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=