mirror of
https://github.com/henrygd/beszel.git
synced 2026-04-04 03:51:49 +02:00
299 lines
7.0 KiB
Go
299 lines
7.0 KiB
Go
//go:build windows
|
|
|
|
// Most of the Windows battery code is based on
|
|
// distatus/battery by Karol 'Kenji Takahashi' Woźniak
|
|
|
|
package battery
|
|
|
|
import (
|
|
"errors"
|
|
"log/slog"
|
|
"math"
|
|
"sync"
|
|
"syscall"
|
|
"unsafe"
|
|
|
|
"golang.org/x/sys/windows"
|
|
)
|
|
|
|
type batteryQueryInformation struct {
|
|
BatteryTag uint32
|
|
InformationLevel int32
|
|
AtRate int32
|
|
}
|
|
|
|
type batteryInformation struct {
|
|
Capabilities uint32
|
|
Technology uint8
|
|
Reserved [3]uint8
|
|
Chemistry [4]uint8
|
|
DesignedCapacity uint32
|
|
FullChargedCapacity uint32
|
|
DefaultAlert1 uint32
|
|
DefaultAlert2 uint32
|
|
CriticalBias uint32
|
|
CycleCount uint32
|
|
}
|
|
|
|
type batteryWaitStatus struct {
|
|
BatteryTag uint32
|
|
Timeout uint32
|
|
PowerState uint32
|
|
LowCapacity uint32
|
|
HighCapacity uint32
|
|
}
|
|
|
|
type batteryStatus struct {
|
|
PowerState uint32
|
|
Capacity uint32
|
|
Voltage uint32
|
|
Rate int32
|
|
}
|
|
|
|
type winGUID struct {
|
|
Data1 uint32
|
|
Data2 uint16
|
|
Data3 uint16
|
|
Data4 [8]byte
|
|
}
|
|
|
|
type spDeviceInterfaceData struct {
|
|
cbSize uint32
|
|
InterfaceClassGuid winGUID
|
|
Flags uint32
|
|
Reserved uint
|
|
}
|
|
|
|
var guidDeviceBattery = winGUID{
|
|
0x72631e54,
|
|
0x78A4,
|
|
0x11d0,
|
|
[8]byte{0xbc, 0xf7, 0x00, 0xaa, 0x00, 0xb7, 0xb3, 0x2a},
|
|
}
|
|
|
|
var (
|
|
setupapi = &windows.LazyDLL{Name: "setupapi.dll", System: true}
|
|
setupDiGetClassDevsW = setupapi.NewProc("SetupDiGetClassDevsW")
|
|
setupDiEnumDeviceInterfaces = setupapi.NewProc("SetupDiEnumDeviceInterfaces")
|
|
setupDiGetDeviceInterfaceDetailW = setupapi.NewProc("SetupDiGetDeviceInterfaceDetailW")
|
|
setupDiDestroyDeviceInfoList = setupapi.NewProc("SetupDiDestroyDeviceInfoList")
|
|
)
|
|
|
|
// winBatteryGet reads one battery by index. Returns (fullCapacity, currentCapacity, state, error).
|
|
// Returns error == errNotFound when there are no more batteries.
|
|
var errNotFound = errors.New("no more batteries")
|
|
|
|
func setupDiSetup(proc *windows.LazyProc, nargs, a1, a2, a3, a4, a5, a6 uintptr) (uintptr, error) {
|
|
_ = nargs
|
|
r1, _, errno := syscall.SyscallN(proc.Addr(), a1, a2, a3, a4, a5, a6)
|
|
if windows.Handle(r1) == windows.InvalidHandle {
|
|
if errno != 0 {
|
|
return 0, error(errno)
|
|
}
|
|
return 0, syscall.EINVAL
|
|
}
|
|
return r1, nil
|
|
}
|
|
|
|
func setupDiCall(proc *windows.LazyProc, nargs, a1, a2, a3, a4, a5, a6 uintptr) syscall.Errno {
|
|
_ = nargs
|
|
r1, _, errno := syscall.SyscallN(proc.Addr(), a1, a2, a3, a4, a5, a6)
|
|
if r1 == 0 {
|
|
if errno != 0 {
|
|
return errno
|
|
}
|
|
return syscall.EINVAL
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func readWinBatteryState(powerState uint32) uint8 {
|
|
switch {
|
|
case powerState&0x00000004 != 0:
|
|
return stateCharging
|
|
case powerState&0x00000008 != 0:
|
|
return stateEmpty
|
|
case powerState&0x00000002 != 0:
|
|
return stateDischarging
|
|
case powerState&0x00000001 != 0:
|
|
return stateFull
|
|
default:
|
|
return stateUnknown
|
|
}
|
|
}
|
|
|
|
func winBatteryGet(idx int) (full, current uint32, state uint8, err error) {
|
|
hdev, err := setupDiSetup(
|
|
setupDiGetClassDevsW,
|
|
4,
|
|
uintptr(unsafe.Pointer(&guidDeviceBattery)),
|
|
0, 0,
|
|
2|16, // DIGCF_PRESENT|DIGCF_DEVICEINTERFACE
|
|
0, 0,
|
|
)
|
|
if err != nil {
|
|
return 0, 0, stateUnknown, err
|
|
}
|
|
defer syscall.SyscallN(setupDiDestroyDeviceInfoList.Addr(), hdev)
|
|
|
|
var did spDeviceInterfaceData
|
|
did.cbSize = uint32(unsafe.Sizeof(did))
|
|
errno := setupDiCall(
|
|
setupDiEnumDeviceInterfaces,
|
|
5,
|
|
hdev, 0,
|
|
uintptr(unsafe.Pointer(&guidDeviceBattery)),
|
|
uintptr(idx),
|
|
uintptr(unsafe.Pointer(&did)),
|
|
0,
|
|
)
|
|
if errno == 259 { // ERROR_NO_MORE_ITEMS
|
|
return 0, 0, stateUnknown, errNotFound
|
|
}
|
|
if errno != 0 {
|
|
return 0, 0, stateUnknown, errno
|
|
}
|
|
|
|
var cbRequired uint32
|
|
errno = setupDiCall(
|
|
setupDiGetDeviceInterfaceDetailW,
|
|
6,
|
|
hdev,
|
|
uintptr(unsafe.Pointer(&did)),
|
|
0, 0,
|
|
uintptr(unsafe.Pointer(&cbRequired)),
|
|
0,
|
|
)
|
|
if errno != 0 && errno != 122 { // ERROR_INSUFFICIENT_BUFFER
|
|
return 0, 0, stateUnknown, errno
|
|
}
|
|
didd := make([]uint16, cbRequired/2)
|
|
cbSize := (*uint32)(unsafe.Pointer(&didd[0]))
|
|
if unsafe.Sizeof(uint(0)) == 8 {
|
|
*cbSize = 8
|
|
} else {
|
|
*cbSize = 6
|
|
}
|
|
errno = setupDiCall(
|
|
setupDiGetDeviceInterfaceDetailW,
|
|
6,
|
|
hdev,
|
|
uintptr(unsafe.Pointer(&did)),
|
|
uintptr(unsafe.Pointer(&didd[0])),
|
|
uintptr(cbRequired),
|
|
uintptr(unsafe.Pointer(&cbRequired)),
|
|
0,
|
|
)
|
|
if errno != 0 {
|
|
return 0, 0, stateUnknown, errno
|
|
}
|
|
devicePath := &didd[2:][0]
|
|
|
|
handle, err := windows.CreateFile(
|
|
devicePath,
|
|
windows.GENERIC_READ|windows.GENERIC_WRITE,
|
|
windows.FILE_SHARE_READ|windows.FILE_SHARE_WRITE,
|
|
nil,
|
|
windows.OPEN_EXISTING,
|
|
windows.FILE_ATTRIBUTE_NORMAL,
|
|
0,
|
|
)
|
|
if err != nil {
|
|
return 0, 0, stateUnknown, err
|
|
}
|
|
defer windows.CloseHandle(handle)
|
|
|
|
var dwOut uint32
|
|
var dwWait uint32
|
|
var bqi batteryQueryInformation
|
|
err = windows.DeviceIoControl(
|
|
handle,
|
|
2703424, // IOCTL_BATTERY_QUERY_TAG
|
|
(*byte)(unsafe.Pointer(&dwWait)),
|
|
uint32(unsafe.Sizeof(dwWait)),
|
|
(*byte)(unsafe.Pointer(&bqi.BatteryTag)),
|
|
uint32(unsafe.Sizeof(bqi.BatteryTag)),
|
|
&dwOut, nil,
|
|
)
|
|
if err != nil || bqi.BatteryTag == 0 {
|
|
return 0, 0, stateUnknown, errors.New("battery tag not returned")
|
|
}
|
|
|
|
var bi batteryInformation
|
|
if err = windows.DeviceIoControl(
|
|
handle,
|
|
2703428, // IOCTL_BATTERY_QUERY_INFORMATION
|
|
(*byte)(unsafe.Pointer(&bqi)),
|
|
uint32(unsafe.Sizeof(bqi)),
|
|
(*byte)(unsafe.Pointer(&bi)),
|
|
uint32(unsafe.Sizeof(bi)),
|
|
&dwOut, nil,
|
|
); err != nil {
|
|
return 0, 0, stateUnknown, err
|
|
}
|
|
|
|
bws := batteryWaitStatus{BatteryTag: bqi.BatteryTag}
|
|
var bs batteryStatus
|
|
if err = windows.DeviceIoControl(
|
|
handle,
|
|
2703436, // IOCTL_BATTERY_QUERY_STATUS
|
|
(*byte)(unsafe.Pointer(&bws)),
|
|
uint32(unsafe.Sizeof(bws)),
|
|
(*byte)(unsafe.Pointer(&bs)),
|
|
uint32(unsafe.Sizeof(bs)),
|
|
&dwOut, nil,
|
|
); err != nil {
|
|
return 0, 0, stateUnknown, err
|
|
}
|
|
|
|
if bs.Capacity == 0xffffffff { // BATTERY_UNKNOWN_CAPACITY
|
|
return 0, 0, stateUnknown, errors.New("battery capacity unknown")
|
|
}
|
|
|
|
return bi.FullChargedCapacity, bs.Capacity, readWinBatteryState(bs.PowerState), nil
|
|
}
|
|
|
|
// HasReadableBattery checks if the system has a battery and returns true if it does.
|
|
var HasReadableBattery = sync.OnceValue(func() bool {
|
|
systemHasBattery := false
|
|
full, _, _, err := winBatteryGet(0)
|
|
if err == nil && full > 0 {
|
|
systemHasBattery = true
|
|
}
|
|
if !systemHasBattery {
|
|
slog.Debug("No battery found", "err", err)
|
|
}
|
|
return systemHasBattery
|
|
})
|
|
|
|
// GetBatteryStats returns the current battery percent and charge state.
|
|
func GetBatteryStats() (batteryPercent uint8, batteryState uint8, err error) {
|
|
if !HasReadableBattery() {
|
|
return batteryPercent, batteryState, errors.ErrUnsupported
|
|
}
|
|
|
|
totalFull := uint32(0)
|
|
totalCurrent := uint32(0)
|
|
batteryState = math.MaxUint8
|
|
|
|
for i := 0; ; i++ {
|
|
full, current, state, bErr := winBatteryGet(i)
|
|
if errors.Is(bErr, errNotFound) {
|
|
break
|
|
}
|
|
if bErr != nil || full == 0 {
|
|
continue
|
|
}
|
|
totalFull += full
|
|
totalCurrent += min(current, full)
|
|
batteryState = state
|
|
}
|
|
|
|
if totalFull == 0 || batteryState == math.MaxUint8 {
|
|
return batteryPercent, batteryState, errors.New("no battery capacity")
|
|
}
|
|
|
|
batteryPercent = uint8(float64(totalCurrent) / float64(totalFull) * 100)
|
|
return batteryPercent, batteryState, nil
|
|
}
|