# Powerbank Category

## Definition

A **powerbank** is a device that has a battery *and* delivers power to other devices. It is conceptually both a charged device (when it is being charged) and a charger (when it is supplying another device's session). For that reason, in this app, a powerbank is a **first-class entity** — separate from `ChargedDevice` and from chargers (which are `ChargedDevice` rows with `deviceClass = .charger`).

A powerbank can sit on **either side of the meter**:

- **Subject side (input)** — the powerbank is being charged. The session looks like any device-charging session, except its subject is the powerbank.
- **Source side (output)** — the powerbank is supplying energy to a device that is being charged. The session's subject is that device; the powerbank fills the source slot (the same slot a charger fills for wireless sessions).

A powerbank can also be both at once if there are two meters in the line (pass-through case): one session on the input meter with the powerbank as subject, one session on the output meter with the powerbank as source. They are independent records linked only by their shared powerbank reference.

## Battery level reporting

Powerbanks report their battery in heterogeneous ways:

- **`.percent`** — 0–100% (treated like any device).
- **`.bars`** — discrete steps, e.g. 4 of 4. The `batteryBarsCount` attribute drives the resolution. Stored canonically as percent (`100 / barsCount * barIndex`); the bars value is also stored on the checkpoint for fidelity.
- **`.fullOnly`** — a single LED that lights only when charging completes. The only honest checkpoint is the 100% anchor; the editor renders a "Full LED is on" affordance and stores `batteryPercent = 100`. Capacity learning treats two consecutive full markers as the bounds of a discharge-then-recharge cycle and uses the source-side energy between them as apparent capacity.
- **`.none`** — no battery level visible. Powerbank-side checkpoints are disabled; capacity learning relies only on full-cycle session energy.

The reason for keeping `.fullOnly` distinct from `.bars` with `count = 1` is data honesty: a 1-bar gauge would invite "0/1 = not full" entries as if they were datapoints, when in reality "not full" is uninformative (the battery could be anywhere from 0% to 99%). The `.fullOnly` editor only emits the precise signal and skips the meaningless one.

This is configured in `PowerbankEditorSheetView`. The checkpoint editor (`BatteryCheckpointEditorContentView`) adapts: text input for percent, stepper for bars, disabled state with a hint for none.

## Sessions and the source slot

A `ChargeSession` carries:

- one **subject**: either `chargedDeviceID` (existing) or `chargedPowerbankID` (new, when the powerbank itself is being charged)
- at most one **source**: `chargerID` (existing) or `sourcePowerbankID` (new). May be empty.

The subject and source columns are mutually exclusive within their pair (i.e. a session cannot have both a `chargedDeviceID` and a `chargedPowerbankID`; same for chargers vs powerbanks on the source side). The store enforces this at session-creation time.

The session-start UI (`MeterChargeRecordTabView`) presents a **unified Source picker**:

- For wireless transport: shows None + chargers + powerbanks.
- For wired transport: shows None + powerbanks (chargers don't apply to wired).
- Source is optional; "None" is always valid.

## Multiple devices, one powerbank

A powerbank can charge several devices simultaneously, each on its own meter. Each meter has its own `ChargeSession` referencing the same `sourcePowerbankID`. The powerbank's view aggregates across them at view time (no cached aggregate curve in storage). Per-session data stays per-session — this is the **curve duplication** rule: each device's curve lives independently, and the powerbank's visualization sums concurrent curves on the fly.

The "one open session per meter" healing invariant ([Charge Session Integrity and Conflict Healing.md](Charge%20Session%20Integrity%20and%20Conflict%20Healing.md)) is unchanged: the source-side reference does not affect grouping. Two devices on two meters with the same powerbank source = two independent open sessions, no conflict.

## Checkpoints with two subjects

`ChargeCheckpoint` was extended with:

- `powerbankID: String?` — populated when the checkpoint reflects the powerbank's battery state. Mutually exclusive with `chargedDeviceID`.
- `batteryBarsValue: Int16` — the as-reported bars value (0 when not in bars mode).

Powerbank-side checkpoints **do not** update the session's `startBatteryPercent` / `endBatteryPercent` fields — those track the device subject only. They also don't trigger device-side capacity learning. They are read at view time when computing powerbank-derived metrics.

The subject toggle appears in the inline checkpoint editor only when the active session's source is a powerbank with `.percent` or `.bars` reporting.

## Derived metrics

Computed view-side at every powerbank summary materialization in `ChargeInsightsStore.fetchPowerbankSummaries()`:

- **Voltage profile (`sourceVoltageMaxCurrents`)** — bucket source-side sessions by `selectedSourceVoltageVolts` rounded to 0.5V. Per bucket, track the maximum `maximumObservedCurrentAmps`. Surfaces what voltage steps the powerbank actually delivers and at what currents.
- **`sourceMaximumPowerWatts`** — max of `maximumObservedPowerWatts` across all source-side sessions.
- **`sourceEfficiencyFactor`** — `Σ Wh delivered (as source) / Σ Wh received (as subject)`. Computed only when both totals exceed 0.5 Wh.
- **`apparentCapacityWh`** — best-effort: pick the most recent pair of powerbank-side checkpoints with ≥ 30 percent delta and sum the source-side energy across overlapping sessions in that window.

Persistent fields with the same names exist on the `Powerbank` entity for future write-back; the materialization currently prefers the derived value and falls back to the persisted value when derivation isn't possible.

## Migration

Schema version: **USB_Meter 19** (additive — new entity + new optional attributes only; lightweight migration).

Legacy `ChargedDevice` rows with `deviceClass = "powerbank"` are not yet migrated automatically. They continue to render as ordinary charged devices for now. A one-time migration that promotes them to the new `Powerbank` entity is planned but deferred until there are real legacy rows in CloudKit to test against — the current shape works correctly for clean installs and for upgraders who haven't yet created any class-`.powerbank` `ChargedDevice` rows.

## Files

- Schema: `USB Meter/Model/CKModel.xcdatamodeld/USB_Meter 19.xcdatamodel/`
- Model layer: `USB Meter/Model/ChargeInsightsModel.swift` (`PowerbankSummary`, `BatteryLevelReporting`, `CheckpointSubject`, `ChargeSessionSource`)
- Store: `USB Meter/Model/ChargeInsightsStore.swift` (`createPowerbank`, `updatePowerbank`, `deletePowerbank`, `fetchPowerbankSummaries`, `derivedPowerbankMetrics`)
- AppData: `USB Meter/Model/AppData.swift` (`powerbankSummaries`, CRUD wrappers, extended `startChargeSession`/`addBatteryCheckpoint`)
- Views/Powerbanks/: `PowerbankEditorSheetView.swift`, `PowerbankDetailView.swift`
- Views/Sidebar/: `PowerbankSidebarCardView.swift`, `SidebarPowerbanksSectionView.swift`
- Source picker: `USB Meter/Views/Meter/Tabs/ChargeRecord/MeterChargeRecordTabView.swift`
- Checkpoint editor: `USB Meter/Views/ChargedDevices/Sheets/ChargeSession/BatteryCheckpointEditorSheetView.swift`
