Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 25 additions & 6 deletions internal/battery/sysfs.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
"strings"
)

const PowerSupplyBase = "/sys/class/power_supply"
var PowerSupplyBase = "/sys/class/power_supply"

func SysfsReadString(path string) (string, error) {
data, err := os.ReadFile(path)
Expand All @@ -35,7 +35,15 @@ func SysfsWriteInt(path string, value int) error {
}

func SysfsWriteString(path string, value string) error {
if err := os.WriteFile(path, []byte(value), 0644); err != nil {
f, err := os.OpenFile(path, os.O_WRONLY|os.O_TRUNC, 0)
if err != nil {
return fmt.Errorf("writing %q to %s: %w", value, path, err)
}
if _, err := f.WriteString(value); err != nil {
f.Close()
return fmt.Errorf("writing %q to %s: %w", value, path, err)
}
if err := f.Close(); err != nil {
return fmt.Errorf("writing %q to %s: %w", value, path, err)
}
return nil
Expand All @@ -62,14 +70,25 @@ func ListBatteries() []string {
}
var bats []string
for _, e := range entries {
typePath := filepath.Join(PowerSupplyBase, e.Name(), "type")
t, err := SysfsReadString(typePath)
t, err := SysfsReadString(filepath.Join(PowerSupplyBase, e.Name(), "type"))
if err != nil {
continue
}
if strings.EqualFold(t, "battery") {
bats = append(bats, e.Name())
if !strings.EqualFold(t, "battery") {
continue
}
if IsDeviceScoped(e.Name()) {
continue
}
bats = append(bats, e.Name())
}
return bats
}

func IsDeviceScoped(name string) bool {
scope, err := SysfsReadString(filepath.Join(PowerSupplyBase, name, "scope"))
if err != nil {
return false
}
return strings.EqualFold(scope, "device")
}
85 changes: 85 additions & 0 deletions internal/battery/sysfs_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package battery

import (
"errors"
"io/fs"
"os"
"path/filepath"
"testing"
Expand Down Expand Up @@ -107,6 +109,7 @@ func TestSysfsReadInt(t *testing.T) {
func TestSysfsWriteInt(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "write_int")
os.WriteFile(path, []byte("0"), 0644)

if err := SysfsWriteInt(path, 80); err != nil {
t.Fatalf("unexpected error: %v", err)
Expand All @@ -124,6 +127,7 @@ func TestSysfsWriteInt(t *testing.T) {
func TestSysfsWriteString(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "write_str")
os.WriteFile(path, []byte("inhibit-charge"), 0644)

if err := SysfsWriteString(path, "auto"); err != nil {
t.Fatalf("unexpected error: %v", err)
Expand Down Expand Up @@ -155,3 +159,84 @@ func TestSysfsExists(t *testing.T) {
}
})
}

func TestSysfsWriteStringMissingAttribute(t *testing.T) {
path := filepath.Join(t.TempDir(), "charge_control_start_threshold")

err := SysfsWriteString(path, "40")
if err == nil {
t.Fatal("expected an error writing to a missing attribute")
}
if !errors.Is(err, fs.ErrNotExist) {
t.Fatalf("got %v, want a not-exist error", err)
}
if errors.Is(err, fs.ErrPermission) {
t.Fatalf("missing attribute reported as a permission error: %v", err)
}
if SysfsExists(path) {
t.Fatal("SysfsWriteString must not create the attribute")
}
}

func writeFixture(t *testing.T, dir, name string, attrs map[string]string) {
t.Helper()
d := filepath.Join(dir, name)
if err := os.MkdirAll(d, 0755); err != nil {
t.Fatal(err)
}
for k, v := range attrs {
if err := os.WriteFile(filepath.Join(d, k), []byte(v+"\n"), 0644); err != nil {
t.Fatal(err)
}
}
}

func TestListBatteries(t *testing.T) {
dir := t.TempDir()
writeFixture(t, dir, "AC", map[string]string{"type": "Mains"})
writeFixture(t, dir, "BAT0", map[string]string{"type": "Battery"})
writeFixture(t, dir, "BAT1", map[string]string{"type": "Battery", "scope": "System"})
writeFixture(t, dir, "hidpp_battery_0", map[string]string{"type": "Battery", "scope": "Device"})
writeFixture(t, dir, "ucsi-source-psy-USBC000:001", map[string]string{"type": "USB", "scope": "Device"})

orig := PowerSupplyBase
PowerSupplyBase = dir
defer func() { PowerSupplyBase = orig }()

got := ListBatteries()
want := []string{"BAT0", "BAT1"}
if len(got) != len(want) {
t.Fatalf("ListBatteries() = %v, want %v", got, want)
}
for i := range want {
if got[i] != want[i] {
t.Fatalf("ListBatteries() = %v, want %v", got, want)
}
}
}

func TestIsDeviceScoped(t *testing.T) {
dir := t.TempDir()
writeFixture(t, dir, "BAT0", map[string]string{"type": "Battery"})
writeFixture(t, dir, "hidpp_battery_0", map[string]string{"type": "Battery", "scope": "Device"})
writeFixture(t, dir, "BAT1", map[string]string{"type": "Battery", "scope": "System"})

orig := PowerSupplyBase
PowerSupplyBase = dir
defer func() { PowerSupplyBase = orig }()

tests := []struct {
name string
want bool
}{
{"BAT0", false},
{"BAT1", false},
{"hidpp_battery_0", true},
{"nonexistent", false},
}
for _, tt := range tests {
if got := IsDeviceScoped(tt.name); got != tt.want {
t.Errorf("IsDeviceScoped(%q) = %v, want %v", tt.name, got, tt.want)
}
}
}