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:
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.
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.
A ChargeSession carries:
chargedDeviceID (existing) or chargedPowerbankID (new, when the powerbank itself is being charged)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:
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) 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.
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.
Computed view-side at every powerbank summary materialization in ChargeInsightsStore.fetchPowerbankSummaries():
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.
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.
USB Meter/Model/CKModel.xcdatamodeld/USB_Meter 19.xcdatamodel/USB Meter/Model/ChargeInsightsModel.swift (PowerbankSummary, BatteryLevelReporting, CheckpointSubject, ChargeSessionSource)USB Meter/Model/ChargeInsightsStore.swift (createPowerbank, updatePowerbank, deletePowerbank, fetchPowerbankSummaries, derivedPowerbankMetrics)USB Meter/Model/AppData.swift (powerbankSummaries, CRUD wrappers, extended startChargeSession/addBatteryCheckpoint)PowerbankEditorSheetView.swift, PowerbankDetailView.swiftPowerbankSidebarCardView.swift, SidebarPowerbanksSectionView.swiftUSB Meter/Views/Meter/Tabs/ChargeRecord/MeterChargeRecordTabView.swiftUSB Meter/Views/ChargedDevices/Sheets/ChargeSession/BatteryCheckpointEditorSheetView.swift