Skip to content
Open
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
17 changes: 17 additions & 0 deletions cmd/batctl/main.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"errors"
"fmt"
"os"
"strings"
Expand Down Expand Up @@ -134,12 +135,20 @@ func setCmd() *cobra.Command {
return fmt.Errorf("invalid thresholds: %w", err)
}

applied := 0
for _, bat := range bats {
if err := b.SetThresholds(bat, startVal, stopVal); err != nil {
if errors.Is(err, backend.ErrNotChargeable) {
continue
}
return fmt.Errorf("setting thresholds on %s: %w", bat, err)
}
applied++
fmt.Printf("Thresholds set: start=%d%% stop=%d%% on %s\n", startVal, stopVal, bat)
}
if applied == 0 {
return fmt.Errorf("no batteries supporting charge control found")
}
return nil
},
}
Expand Down Expand Up @@ -173,12 +182,20 @@ func applyCmd() *cobra.Command {
bats = []string{cfg.Battery}
}

applied := 0
for _, bat := range bats {
if err := b.SetThresholds(bat, cfg.Start, cfg.Stop); err != nil {
if errors.Is(err, backend.ErrNotChargeable) && cfg.Battery == "all" {
continue
}
return fmt.Errorf("applying thresholds on %s: %w", bat, err)
}
applied++
fmt.Printf("Applied: start=%d%% stop=%d%% on %s\n", cfg.Start, cfg.Stop, bat)
}
if applied == 0 {
return fmt.Errorf("no batteries supporting charge control found")
}
return nil
},
}
Expand Down
3 changes: 3 additions & 0 deletions internal/backend/asus.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ func (b *ASUSBackend) SetThresholds(bat string, start, stop int) error {
if err := b.ValidateThresholds(start, stop); err != nil {
return err
}
if !battery.SysfsExists(battery.BatPath(bat, "charge_control_end_threshold")) {
return ErrNotChargeable
}
return battery.SysfsWriteInt(battery.BatPath(bat, "charge_control_end_threshold"), stop)
}

Expand Down
11 changes: 10 additions & 1 deletion internal/backend/backend.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package backend

import "fmt"
import (
"errors"
"fmt"
)

type Capabilities struct {
StartThreshold bool
Expand All @@ -13,6 +16,12 @@ type Capabilities struct {
StartStopGap int // if non-zero, hardware enforces start = stop - gap (Dell: 5)
}

// ErrNotChargeable is returned by SetThresholds when the target battery does
// not expose any charge control attributes (e.g. HID++ peripheral batteries,
// which appear under /sys/class/power_supply/ with type=Battery but have no
// charge_control_* files).
var ErrNotChargeable = errors.New("battery does not expose charge control attributes")

type Backend interface {
Name() string
Detect() bool
Expand Down
14 changes: 11 additions & 3 deletions internal/backend/dell.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,16 +53,24 @@ func (b *DellBackend) SetThresholds(bat string, start, stop int) error {
if err := b.ValidateThresholds(start, stop); err != nil {
return err
}
if !battery.SupportsChargeControl(bat) {
return ErrNotChargeable
}
chargeTypesPath := battery.BatPath(bat, "charge_types")
if battery.SysfsExists(chargeTypesPath) {
if err := battery.SysfsWriteString(chargeTypesPath, "Custom"); err != nil {
return err
}
}
if err := battery.SysfsWriteInt(battery.BatPath(bat, "charge_control_start_threshold"), start); err != nil {
return err
if battery.SysfsExists(battery.BatPath(bat, "charge_control_start_threshold")) {
if err := battery.SysfsWriteInt(battery.BatPath(bat, "charge_control_start_threshold"), start); err != nil {
return err
}
}
if battery.SysfsExists(battery.BatPath(bat, "charge_control_end_threshold")) {
return battery.SysfsWriteInt(battery.BatPath(bat, "charge_control_end_threshold"), stop)
}
return battery.SysfsWriteInt(battery.BatPath(bat, "charge_control_end_threshold"), stop)
return nil
}

func (b *DellBackend) GetChargeBehaviour(bat string) (current string, available []string, err error) {
Expand Down
9 changes: 7 additions & 2 deletions internal/backend/framework.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,13 @@ func (b *FrameworkBackend) SetThresholds(bat string, start, stop int) error {
if err := b.ValidateThresholds(start, stop); err != nil {
return err
}
if err := battery.SysfsWriteInt(battery.BatPath(bat, "charge_control_end_threshold"), stop); err != nil {
return err
if !battery.SupportsChargeControl(bat) {
return ErrNotChargeable
}
if battery.SysfsExists(battery.BatPath(bat, "charge_control_end_threshold")) {
if err := battery.SysfsWriteInt(battery.BatPath(bat, "charge_control_end_threshold"), stop); err != nil {
return err
}
}
if battery.SysfsExists(battery.BatPath(bat, "charge_control_start_threshold")) {
if err := battery.SysfsWriteInt(battery.BatPath(bat, "charge_control_start_threshold"), start); err != nil {
Expand Down
8 changes: 5 additions & 3 deletions internal/backend/generic.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,15 @@ func (b *GenericBackend) SetThresholds(bat string, start, stop int) error {
if err := b.ValidateThresholds(start, stop); err != nil {
return err
}
caps := b.Capabilities()
if caps.StartThreshold {
if !battery.SupportsChargeControl(bat) {
return ErrNotChargeable
}
if battery.SysfsExists(battery.BatPath(bat, "charge_control_start_threshold")) {
if err := battery.SysfsWriteInt(battery.BatPath(bat, "charge_control_start_threshold"), start); err != nil {
return err
}
}
if caps.StopThreshold {
if battery.SysfsExists(battery.BatPath(bat, "charge_control_end_threshold")) {
if err := battery.SysfsWriteInt(battery.BatPath(bat, "charge_control_end_threshold"), stop); err != nil {
return err
}
Expand Down
3 changes: 3 additions & 0 deletions internal/backend/lg.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ func (b *LGBackend) SetThresholds(bat string, start, stop int) error {
if b.legacy {
return battery.SysfsWriteInt(lgBatteryCareLimitPath, stop)
}
if !battery.SysfsExists(battery.BatPath(bat, "charge_control_end_threshold")) {
return ErrNotChargeable
}
return battery.SysfsWriteInt(battery.BatPath(bat, "charge_control_end_threshold"), stop)
}

Expand Down
3 changes: 3 additions & 0 deletions internal/backend/msi.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ func (b *MSIBackend) SetThresholds(bat string, start, stop int) error {
if err := b.ValidateThresholds(start, stop); err != nil {
return err
}
if !battery.SysfsExists(battery.BatPath(bat, "charge_control_end_threshold")) {
return ErrNotChargeable
}
return battery.SysfsWriteInt(battery.BatPath(bat, "charge_control_end_threshold"), stop)
}

Expand Down
11 changes: 8 additions & 3 deletions internal/backend/surface.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,13 +61,18 @@ func (b *SurfaceBackend) SetThresholds(bat string, start, stop int) error {
if err := b.ValidateThresholds(start, stop); err != nil {
return err
}
caps := b.Capabilities()
if caps.StartThreshold {
if !battery.SupportsChargeControl(bat) {
return ErrNotChargeable
}
if battery.SysfsExists(battery.BatPath(bat, "charge_control_start_threshold")) {
if err := battery.SysfsWriteInt(battery.BatPath(bat, "charge_control_start_threshold"), start); err != nil {
return err
}
}
return battery.SysfsWriteInt(battery.BatPath(bat, "charge_control_end_threshold"), stop)
if battery.SysfsExists(battery.BatPath(bat, "charge_control_end_threshold")) {
return battery.SysfsWriteInt(battery.BatPath(bat, "charge_control_end_threshold"), stop)
}
return nil
}

func (b *SurfaceBackend) GetChargeBehaviour(bat string) (current string, available []string, err error) {
Expand Down
14 changes: 11 additions & 3 deletions internal/backend/system76.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,18 @@ func (b *System76Backend) SetThresholds(bat string, start, stop int) error {
if err := b.ValidateThresholds(start, stop); err != nil {
return err
}
if err := battery.SysfsWriteInt(battery.BatPath(bat, "charge_control_start_threshold"), start); err != nil {
return err
if !battery.SupportsChargeControl(bat) {
return ErrNotChargeable
}
if battery.SysfsExists(battery.BatPath(bat, "charge_control_start_threshold")) {
if err := battery.SysfsWriteInt(battery.BatPath(bat, "charge_control_start_threshold"), start); err != nil {
return err
}
}
return battery.SysfsWriteInt(battery.BatPath(bat, "charge_control_end_threshold"), stop)
if battery.SysfsExists(battery.BatPath(bat, "charge_control_end_threshold")) {
return battery.SysfsWriteInt(battery.BatPath(bat, "charge_control_end_threshold"), stop)
}
return nil
}

func (b *System76Backend) GetChargeBehaviour(bat string) (current string, available []string, err error) {
Expand Down
14 changes: 11 additions & 3 deletions internal/backend/thinkpad.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,18 @@ func (b *ThinkPadBackend) SetThresholds(bat string, start, stop int) error {
if err := b.ValidateThresholds(start, stop); err != nil {
return err
}
if err := battery.SysfsWriteInt(battery.BatPath(bat, "charge_control_start_threshold"), start); err != nil {
return err
if !battery.SupportsChargeControl(bat) {
return ErrNotChargeable
}
if battery.SysfsExists(battery.BatPath(bat, "charge_control_start_threshold")) {
if err := battery.SysfsWriteInt(battery.BatPath(bat, "charge_control_start_threshold"), start); err != nil {
return err
}
}
return battery.SysfsWriteInt(battery.BatPath(bat, "charge_control_end_threshold"), stop)
if battery.SysfsExists(battery.BatPath(bat, "charge_control_end_threshold")) {
return battery.SysfsWriteInt(battery.BatPath(bat, "charge_control_end_threshold"), stop)
}
return nil
}

func (b *ThinkPadBackend) GetChargeBehaviour(bat string) (current string, available []string, err error) {
Expand Down
3 changes: 3 additions & 0 deletions internal/backend/toshiba.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ func (b *ToshibaBackend) SetThresholds(bat string, start, stop int) error {
if err := b.ValidateThresholds(start, stop); err != nil {
return err
}
if !battery.SysfsExists(battery.BatPath(bat, "charge_control_end_threshold")) {
return ErrNotChargeable
}
return battery.SysfsWriteInt(battery.BatPath(bat, "charge_control_end_threshold"), stop)
}

Expand Down
14 changes: 11 additions & 3 deletions internal/backend/tuxedo.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,16 +66,24 @@ func (b *TuxedoBackend) SetThresholds(bat string, start, stop int) error {
if err := b.ValidateThresholds(start, stop); err != nil {
return err
}
if !battery.SupportsChargeControl(bat) {
return ErrNotChargeable
}
chargeTypePath := battery.BatPath(bat, "charge_type")
if battery.SysfsExists(chargeTypePath) {
if err := battery.SysfsWriteString(chargeTypePath, "Custom"); err != nil {
return err
}
}
if err := battery.SysfsWriteInt(battery.BatPath(bat, "charge_control_start_threshold"), start); err != nil {
return err
if battery.SysfsExists(battery.BatPath(bat, "charge_control_start_threshold")) {
if err := battery.SysfsWriteInt(battery.BatPath(bat, "charge_control_start_threshold"), start); err != nil {
return err
}
}
if battery.SysfsExists(battery.BatPath(bat, "charge_control_end_threshold")) {
return battery.SysfsWriteInt(battery.BatPath(bat, "charge_control_end_threshold"), stop)
}
return battery.SysfsWriteInt(battery.BatPath(bat, "charge_control_end_threshold"), stop)
return nil
}

func (b *TuxedoBackend) GetChargeBehaviour(bat string) (current string, available []string, err error) {
Expand Down
7 changes: 7 additions & 0 deletions internal/battery/sysfs.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,10 @@ func ListBatteries() []string {
}
return bats
}

// SupportsChargeControl reports whether a power_supply exposes the standard
// charge_control_* threshold attributes.
func SupportsChargeControl(bat string) bool {
return SysfsExists(BatPath(bat, "charge_control_start_threshold")) ||
SysfsExists(BatPath(bat, "charge_control_end_threshold"))
}
16 changes: 13 additions & 3 deletions internal/tui/app.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package tui

import (
"errors"
"fmt"
"os"
"time"
Expand Down Expand Up @@ -336,19 +337,28 @@ func (m *model) applyAndSave() tea.Cmd {

caps := m.backend.Capabilities()

var applied []string
for _, bat := range m.batteries {
if err := m.backend.SetThresholds(bat, m.startVal, m.stopVal); err != nil {
if errors.Is(err, backend.ErrNotChargeable) {
continue
}
return m.setMessage(fmt.Sprintf("Error on %s: %v", bat, err), errorStyle)
}
if caps.ChargeBehaviour && m.behaviourCur != "" {
applied = append(applied, bat)
if caps.ChargeBehaviour && m.behaviourCur != "" &&
battery.SysfsExists(battery.BatPath(bat, "charge_behaviour")) {
if err := m.backend.SetChargeBehaviour(bat, m.behaviourCur); err != nil {
return m.setMessage(fmt.Sprintf("Thresholds set, but behaviour error on %s: %v", bat, err), errorStyle)
}
}
}
if len(applied) == 0 {
return m.setMessage("No batteries support charge control", errorStyle)
}

batName := m.batteries[0]
if len(m.batteries) > 1 {
batName := applied[0]
if len(applied) > 1 {
batName = "all"
}
cfg := persist.Config{Battery: batName, Start: m.startVal, Stop: m.stopVal}
Expand Down