Watch
1
0
Fork
You've already forked SouveraineOS
0

task-33: battery and charging belong to the device state machine

This commit is contained in:
Fimeg 2026-07-26 16:37:28 -04:00
commit 608a9520e0
2 changed files with 85 additions and 0 deletions

View file

@ -0,0 +1,84 @@
# TASK 33 — Battery and charging belong to the device state machine
**Status:** open, raised 2026-07-26. **Size:** one session for (1), one for
(2); (3) is a policy decision before it is code.
**Repos:** `~/Projects/souveraine` (`packaging/upower-souveraine`,
`surfaces/quickshell`, `src/sessiond`).
## Why this exists
Charging has no owner. Every surface reads UPower directly and renders
whatever it says, so the phone shows things that are true field-by-field and
wrong as a sentence: it reaches 100%, the charger terminates, and the lock
screen counts down from 94% with the cable still plugged in. Nothing is
broken — that is charge-termination hysteresis, the charger resting between
top-ups — but nothing in the stack knows enough to say so, because nothing
owns the question.
Every other device signal was moved behind the state machine for exactly
this reason (`DEVICE-STATE-MACHINE.md`): a sensor reports, the machine
decides, one surface renders the decision. Battery is the last raw feed
still going straight to the glass.
## What is already true (verified live 2026-07-26, blueline)
- The UPower fork already publishes `ChargeType` on the battery device by
walking the supplier device link to `pmi8998-charger` — the fuel gauge
(`qcom-battery`) has no `charge_type` at all. Live value reads back over
D-Bus, `emits-change`.
- The shell now renders it: `services/ChargeRate.qml` → "Fast charging",
"Slow charging", plus `pluggedNotCharging` so the lock surface says
"Charged 99%" instead of a countdown. That is the *display* fix. It is
not the logic fix, and it is not in the state machine.
- **`/sys/class/power_supply/pmi8998-charger/charge_control_end_threshold`
exists, is writable, and currently reads `99`.** A charge ceiling is
available on this hardware today with no kernel work.
- UPower nonetheless reports `ChargeThresholdSupported: false`,
`ChargeEndThreshold: 0`. Upstream looks for the threshold attributes on
the *battery* device; here they live on the charger — the identical
supplier-walk bug the fork already fixed for `charge_type`.
## What to build
1. **Threshold support in the fork, the same way charge_type was done.**
Reuse `up_device_supply_get_supplier_charge_type_str`'s device-link walk
for `charge_control_{start,end}_threshold`, so
`ChargeThresholdSupported` becomes true and `ChargeEndThreshold` reads
and writes through the standard UPower API. No new interface — the one
upstream already defines, finally reporting the truth on this device.
Write path needs a polkit action; the shell is the agent for it.
2. **Charging as evidence, not as a display feed.** `device_state` already
carries sensor evidence and health (`§10`). Charging state belongs
there: plugged/unplugged, charge type, whether the charger is resting or
working, and — the part no surface can currently derive — *what the
machine concludes*, so "Charged", "Charging slowly", and "the cable is in
but nothing is happening and that is now unusual" are one decision made
once rather than three surfaces guessing. `sensors_degraded` (TASK-08 f)
is the precedent for the shape.
3. **A charge policy, and who is allowed to set it.** Casey's framing: the
agent might one day decide to let the pack run to 30% first, or hold it
at 80% overnight. The mechanism now exists (1). The decision that does
not exist is authority: a ceiling that the device sets for battery health
is not the same as a ceiling an agent chose, and neither is the same as
one the user asked for out loud. `SETTINGS-AUTHORITY.md` already has the
risk tiers and propose-vs-set split; charge policy is a natural
step-up-gated setting under it. **Decide the tier before writing the
verb** — a programmable ceiling that silently strands the phone at 30%
before a trip is a worse failure than never having built it.
## Do not
- Add a fourth surface that reads UPower directly. That is the problem.
- Ship a writable ceiling before (3) has an answer.
- Treat "discharging while plugged in" as a fault to alarm about. It is the
normal resting state of a full pack.
## Connects to
`DEVICE-STATE-MACHINE.md` (§10 health, the evidence model), TASK-08 (the
state manager and its two unsurfaced findings), `SETTINGS-AUTHORITY.md`
(risk tiers), TASK-30 (whatever verbs this exposes carry the same five
refusal codes), `upower-charge-type-sdm845` memory (the supplier-walk fix
this reuses).

View file

@ -36,6 +36,7 @@ plus open threads from the 2026-07-17 session.
| 30 | [Agent reach: verb tables across the surfaces](30-agent-reach-verb-tables.md) | half built 07-26 | sessiond has describe + refusal codes + intent; dock/apps/shell/settings do not |
| 31 | [The radial dial](31-radial-dial.md) | open | component before contents; entries come from the verb tables, not a list in QML |
| 32 | [The pill and the keyboard, as owned apps](32-pill-and-osk-as-apps.md) | open | both erroneous daily; neither has an owner or a shipping story |
| 33 | [Battery and charging belong to the device state machine](33-battery-as-device-state.md) | open | last raw feed going straight to the glass; the charge ceiling is writable today |
| — | [Unify the shell trees](unify-shell-trees-laptop-phone.md) | open | ~900-file `ii` drift; blocks durable shell fixes reaching the phone |
Archived (see `archive/`): 05 crash reporter — done 2026-07-21,