feat(power): re-apply battery charge limit on AC state change
The threshold was written boot-only, so a firmware that clears
charge_control_end_threshold on unplug would silently lose the cap
until the next reboot. Add a services.udev rule on the mains adapter
(ATTR{type}=="Mains", vendor-neutral) that restarts the oneshot via
systemctl --no-block on every AC state change. Uses restart (not
try-restart) so it re-applies even if the boot run was inactive.
Eval-verified: rule present when the charge limit is on, absent when
off; downstream-template-system still evaluates.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -347,7 +347,17 @@ how to override it. Items marked ✓ are shipped.
|
|||||||
turns it on for a `GenuineIntel` CPU.
|
turns it on for a `GenuineIntel` CPU.
|
||||||
- ✓ **longevity:** `power.batteryChargeLimit` (e.g. 80) — a backend-
|
- ✓ **longevity:** `power.batteryChargeLimit` (e.g. 80) — a backend-
|
||||||
independent sysfs oneshot writes `charge_control_end_threshold`. Off by
|
independent sysfs oneshot writes `charge_control_end_threshold`. Off by
|
||||||
default; the installer scaffolds it commented-out on laptops.
|
default; the installer scaffolds it commented-out on laptops. ✓ the
|
||||||
|
threshold is now **re-applied on AC state changes** (a `services.udev`
|
||||||
|
rule on `SUBSYSTEM=="power_supply", ATTR{type}=="Mains"` restarts the
|
||||||
|
oneshot via `systemctl --no-block`), closing the firmware-resets-on-
|
||||||
|
unplug gap — the match is by adapter *type*, not kernel name
|
||||||
|
(AC/AC0/ADP1/ACAD vary), and `restart` (not `try-restart`) re-applies
|
||||||
|
even if the boot run was inactive. Eval-verified both ways (rule present
|
||||||
|
with charge limit on / absent off). **Pending an on-hardware check** (a
|
||||||
|
`sudo nixos-rebuild` + a physical unplug to fire the udev event — the
|
||||||
|
dev box has both an `AC` Mains adapter and a `BAT0`
|
||||||
|
`charge_control_end_threshold`, so it can exercise it).
|
||||||
- ✓ **installer:** writes `power.laptop = true` (battery probe) and
|
- ✓ **installer:** writes `power.laptop = true` (battery probe) and
|
||||||
`power.thermal.enable` (Intel) into the generated `system.nix`.
|
`power.thermal.enable` (Intel) into the generated `system.nix`.
|
||||||
- ✓ **idle cohesion:** `modules/home/idle.nix` now suspends only on
|
- ✓ **idle cohesion:** `modules/home/idle.nix` now suspends only on
|
||||||
@@ -357,8 +367,6 @@ how to override it. Items marked ✓ are shipped.
|
|||||||
logind's lid handling is left at its defaults (suspend on lid close,
|
logind's lid handling is left at its defaults (suspend on lid close,
|
||||||
ignore when docked) — an explicit "I'm done" that coheres with the
|
ignore when docked) — an explicit "I'm done" that coheres with the
|
||||||
idle behaviour.
|
idle behaviour.
|
||||||
- Remaining: a boot-only→event-driven charge-limit re-apply (udev) if a
|
|
||||||
firmware resets the threshold on unplug.
|
|
||||||
- ✓ **Waybar parity:** the `custom/powerprofile` indicator now shows in the
|
- ✓ **Waybar parity:** the `custom/powerprofile` indicator now shows in the
|
||||||
summer-day/night whole-swap themes too. `powerProfileStatus`/`Cycle` are
|
summer-day/night whole-swap themes too. `powerProfileStatus`/`Cycle` are
|
||||||
named `writeShellScriptBin`s on PATH (`waybar.nix` home.packages), so the
|
named `writeShellScriptBin`s on PATH (`waybar.nix` home.packages), so the
|
||||||
|
|||||||
@@ -109,8 +109,9 @@
|
|||||||
where the hardware exposes a charge threshold
|
where the hardware exposes a charge threshold
|
||||||
(/sys/class/power_supply/BAT*/charge_control_end_threshold).
|
(/sys/class/power_supply/BAT*/charge_control_end_threshold).
|
||||||
null leaves charging at the firmware default. Backend-independent
|
null leaves charging at the firmware default. Backend-independent
|
||||||
(a small systemd unit writes the sysfs knob at boot), so it
|
(a small systemd unit writes the sysfs knob, re-applied on AC
|
||||||
works under PPD too; needs nomarchy.system.power.laptop.
|
state changes), so it works under PPD too; needs
|
||||||
|
nomarchy.system.power.laptop.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -38,10 +38,10 @@ in
|
|||||||
|
|
||||||
# Battery charge limit via sysfs. PPD can't cap charge at all, and
|
# Battery charge limit via sysfs. PPD can't cap charge at all, and
|
||||||
# TLP's own knob only applies under TLP — so a tiny oneshot writes
|
# TLP's own knob only applies under TLP — so a tiny oneshot writes
|
||||||
# the threshold directly, independent of the backend. Boot-time only:
|
# the threshold directly, independent of the backend. Re-applied on
|
||||||
# some firmwares reset the threshold on unplug; revisit with a udev
|
# AC state changes by the udev rule below (some firmwares reset the
|
||||||
# hook if that bites. `-` paths that don't exist are skipped, so this
|
# threshold when the charger is unplugged). `[ -w ]` skips paths that
|
||||||
# is a clean no-op on hardware without the control.
|
# don't exist, so this is a clean no-op on hardware without the control.
|
||||||
systemd.services.nomarchy-battery-charge-limit = lib.mkIf chargeLimit {
|
systemd.services.nomarchy-battery-charge-limit = lib.mkIf chargeLimit {
|
||||||
description = "Cap battery charging at ${toString cfg.batteryChargeLimit}%";
|
description = "Cap battery charging at ${toString cfg.batteryChargeLimit}%";
|
||||||
wantedBy = [ "multi-user.target" ];
|
wantedBy = [ "multi-user.target" ];
|
||||||
@@ -56,5 +56,16 @@ in
|
|||||||
exit 0
|
exit 0
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
# Re-apply the threshold whenever the mains adapter changes state: the
|
||||||
|
# boot oneshot above runs once, but some firmwares clear the limit when
|
||||||
|
# the charger is unplugged, so it must be re-asserted on the event.
|
||||||
|
# Matched by ATTR{type}=="Mains" (vendor-neutral — the kernel name
|
||||||
|
# AC/AC0/ADP1/ACAD varies); --no-block so the RUN+= returns at once
|
||||||
|
# (systemd kills long-running udev workers); restart (not try-restart)
|
||||||
|
# so it re-applies even if the boot run was inactive.
|
||||||
|
services.udev.extraRules = lib.mkIf chargeLimit ''
|
||||||
|
SUBSYSTEM=="power_supply", ATTR{type}=="Mains", RUN+="${config.systemd.package}/bin/systemctl --no-block restart nomarchy-battery-charge-limit.service"
|
||||||
|
'';
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user