Skip to content

Skip device-scoped batteries and stop masking missing sysfs attributes - #15

Closed
jash8506 wants to merge 1 commit into
Ooooze:masterfrom
jash8506:fix/skip-device-scoped-batteries
Closed

jash8506 wants to merge 1 commit into
Ooooze:masterfrom
jash8506:fix/skip-device-scoped-batteries

Conversation

@jash8506

Copy link
Copy Markdown

Fixes #14.

Peripherals with their own cell — wireless keyboards and mice, headsets,
controllers — register in /sys/class/power_supply with type of Battery.
ListBatteries returned them alongside the real system battery, so set,
apply and the TUI tried to write charge thresholds to, for example, a
Logitech keyboard's hidpp_battery_0, which exposes no threshold attributes:

Error on hidpp_battery_0: writing "40" to /sys/class/power_supply/hidpp_battery_0/charge_control_start_threshold: permission denied

Skip device-scoped supplies

The kernel already marks these: it reports POWER_SUPPLY_SCOPE_DEVICE through
the scope attribute. hid-logitech-hidpp hardcodes it, and the file never
exposes charge_control_* at all.

ListBatteries now skips anything device-scoped. A battery with no scope
attribute is still treated as a system battery — scope is optional and most
laptop batteries omit it (my BAT0 does), so this deliberately does not
require the attribute to be present.

I chose scope over "does this battery expose charge_control_*?" precisely
because the latter would break the platform backends: IdeaPad, Huawei, Samsung,
Sony and Acer drive thresholds through a platform device and their batteries
expose no per-battery threshold attributes, so an attribute-based filter would
return an empty list and break them. Filtering on scope leaves them untouched.

Report missing attributes honestly

SysfsWriteString used os.WriteFile, which passes O_CREATE. sysfs refuses
to create files and answers EACCES rather than ENOENT, so a missing
attribute was reported as permission denied even as root — which sends people
looking at privileges instead of at the attribute. The read path in the same run
reported no such file or directory for the identical path.

Opening with O_WRONLY and no O_CREATE surfaces the real error.

Tests

  • TestListBatteries — fixture tree with a scopeless battery, a System
    battery, a Device-scoped hidpp_battery_0, a Mains supply and a
    Device-scoped USB supply; asserts only the first two are returned.
  • TestIsDeviceScoped — scopeless, System, Device, and an unreadable path.
  • TestSysfsWriteStringMissingAttribute — asserts a missing attribute yields
    fs.ErrNotExist, explicitly not fs.ErrPermission, and that the attribute
    is not created.

Both new tests fail against master and pass with this change:

--- FAIL: TestSysfsWriteStringMissingAttribute
    expected an error writing to a missing attribute
--- FAIL: TestListBatteries
    ListBatteries() = [BAT0 BAT1 hidpp_battery_0], want [BAT0 BAT1]

TestSysfsWriteInt and TestSysfsWriteString wrote to paths that did not
exist, relying on the O_CREATE this PR removes. sysfs attributes always exist
ahead of time, so they now pre-create the attribute before writing, which is
what the real thing looks like.

PowerSupplyBase became a var so tests can point it at a fixture directory.

Verification

go build, go vet and go test ./... all pass.

On the affected hardware (ThinkPad, ThinkPad backend), master picks the
peripheral up while the keyboard is connected:

$ batctl detect
Backend: ThinkPad
Batteries: [BAT0 hidpp_battery_0]

The exclusion itself is covered by TestListBatteries, which builds a fixture
tree containing a Device-scoped hidpp_battery_0 and asserts it is dropped;
it fails against master with exactly the list above. I have not captured a
live before/after of the fixed binary with the keyboard reattached — happy to
add that if useful.

Scope

This removes the condition that triggered the failure — the peripheral never
enters the battery list, so the loop in applyAndSave has nothing to fail on.
It does not change how that loop behaves if some other battery fails, which
remains as it is today.


🤖 Generated with Claude Code

Peripherals with their own cell — wireless keyboards and mice, headsets,
controllers — register in /sys/class/power_supply with type "Battery".
ListBatteries returned them alongside the real system battery, so `set`,
`apply` and `persist enable` tried to write charge thresholds to, say, a
Logitech keyboard's hidpp_battery_0, which exposes no threshold attributes:

    Error on hidpp_battery_0: writing "40" to /sys/class/power_supply/
    hidpp_battery_0/charge_control_start_threshold: permission denied

`set` aborts on that error, so the thresholds it had already written to BAT0
were never saved to /etc/batctl.conf and silently reverted on the next boot.
The failure is intermittent because these devices leave sysfs when the
peripheral sleeps or is unplugged.

The kernel already marks such supplies: it reports POWER_SUPPLY_SCOPE_DEVICE
through the "scope" attribute (hid-logitech-hidpp hardcodes it). Skip anything
device-scoped. Batteries without a "scope" attribute are still treated as
system batteries, so laptop batteries — including those on platform backends
such as IdeaPad, Huawei, Samsung, Sony and Acer, which drive thresholds
through a platform device and expose no per-battery attributes — are
unaffected.

The error also lied about the cause. SysfsWriteString used os.WriteFile, which
passes O_CREATE; sysfs refuses to create files and answers EACCES rather than
ENOENT, so a missing attribute looked like a privilege problem even under
sudo. Open without O_CREATE so the real "no such file or directory" surfaces.

The two write tests wrote to paths that did not exist, relying on the
O_CREATE that caused the bug. sysfs attributes always exist ahead of time, so
they now pre-create the attribute, matching real behaviour.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@jash8506

Copy link
Copy Markdown
Author

Closing — this overlaps #13, which predates it by twelve days and fixes the same root cause. I found it only after opening this. Apologies for the noise.

The parts that were additive rather than duplicative are now stacked on top of that branch as lucserre#1: scope-based filtering in ListBatteries (so peripherals don't appear in detect/status, which #13's write-path guards don't cover), and dropping O_CREATE from SysfsWriteString so a missing attribute reports ENOENT rather than EACCES. The second is independent of the pseudo-battery handling and affects every sysfs write.

@lucserre's approach is the right one for the write path — SupportsChargeControl catches a system battery that lacks the attributes, which scope filtering wouldn't.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

set/apply fail on device-scoped peripheral batteries (hidpp_battery_N)

2 participants