Skip to content

Fix capacity over-estimate for charge sessions first seen mid-charge - #132

Merged
dzid26 merged 1 commit into
mainfrom
work/health-session-deltas
Oct 7, 2026
Merged

dzid26 merged 1 commit into
mainfrom
work/health-session-deltas

Conversation

@dzid26

@dzid26 dzid26 commented Oct 7, 2026

Copy link
Copy Markdown
Collaborator

Summary

Closes #114

chargeSessions() paired the SOC swing between the first and last charging samples the app saw with charge_energy_added / charge_miles_added_rated, which the car reports as running totals since its session began. A session first seen mid-charge (app opened late, link dropped, phone away) therefore over-estimated capacity and full range: plug-in at 20 %, first sample at 50 % with 22.5 kWh, last at 80 % with 45 kWh read as 45 / 30 % = 150 kWh instead of 75 kWh. The rated-miles backbone was inflated the same way.

  • Deltas. Energy and rated miles are the growth of each counter from the first to the last sample of a session (last - first). A null first value counts as 0. A counter that goes down inside a session was restarted by the car (for example a stale total on a Starting sample), so the last value is used. The existing > 0 checks stay.
  • Segmentation (documented in the KDoc of chargeSessions()): a session is a run of Charging/Starting samples; a Stopped/NoPower pause shorter than 10 minutes (last charging sample to the resuming one) does not end it; Complete, Disconnected, any other state (missing, Unknown, Calibrating) or a longer pause does. Blips no longer inflate the count behind the learning gate.
  • Moved to core. ChargeSession and chargeSessions() now live in core/.../health/ChargeSession.kt (with MIN_SWING_PERCENT); healthSummary() imports them. BatterySample is untouched. The file is ChargeSession.kt, not ChargeSessions.kt, because detekt's MatchingDeclarationName requires a file with one top-level class to be named after it.
  • Comment fix. The comment above LEARNING_SESSIONS = 3 described the twenty sessions of HIGH_SESSIONS. It now describes the three; the twenty-session sentence moved to HIGH_SESSIONS. Value unchanged.
  • Test dependency. core/build.gradle.kts adds testImplementation(kotlin("test")): ADR-0007 asks new core tests to use kotlin.test, and the new tests do. Existing JUnit tests are unchanged.

Judgment calls worth a look:

  • Silence is not a pause. Two charging samples with nothing between them (link dropped) stay one session however far apart; only Stopped/NoPower samples can start the 10-minute clock. The counters are cumulative, so the delta covers the unobserved stretch.
  • A counter restart is any decrease between consecutive non-null readings, not just last < first, so a stale first value that the new session climbs back past is still caught.

Type

  • Bug fix
  • Feature
  • Protocol / crypto change
  • Docs / CI / tooling
  • Refactor (no behavior change)

Testing

  • ./gradlew :core:test: run as a core-only scratch build, because Google Maven is unreachable here and the app modules cannot be built. :core:test, :core:ktlintCheck and :core:detekt pass (135 tests, 17 of them new in ChargeSessionsTest: mid-charge capacity 75 kWh not 150, counter reset, Starting then Charging, short Stopped blip, long Stopped gap, Complete, Disconnected, and more).
  • Mutation check in the scratch copy: reverting each rule (cumulative last value, no reset detection, no pause bridging, Starting excluded, > instead of >= at the 10-minute boundary, ...) fails at least one new test.
  • ./gradlew :app:testDebugUnitTest: not run. Instead the changed HealthSummary.kt and the unchanged HealthSummaryTest.kt were copied into the scratch core project (scratch only) and compiled, linted and run against the new core code: 6/6 pass. CI still has to compile the app.
  • ./gradlew :app:lintDebug: not run (see above)
  • Real car: not tested

Checklist

  • Protocol changes ship test vectors; expected.txt regenerated, not hand-edited (n/a: no protocol change)
  • No VINs, keys, or decrypted payloads in logs
  • Docs/master-plan updated where relevant (not touched: Replace the history CSV with an append-only raw ChargeState log #94 edits docs/master-plan.md, and the "Battery health v1" item stays open)
  • New source files carry SPDX-License-Identifier: AGPL-3.0-only

🤖 Generated with Claude Code

https://claude.ai/code/session_0129RzhbEE2cboMcYyZMuxqX


Generated by Claude Code

chargeSessions() paired the SOC swing between the first and last charging
samples the app saw with charge_energy_added and charge_miles_added_rated,
which the car reports as running totals since its own session began. A
session first seen mid-charge (app opened late, link dropped, phone away)
therefore over-estimated capacity and full range: plug-in at 20 %, first
sample 50 % with 22.5 kWh, last 80 % with 45 kWh read as 150 kWh, not 75.

- Energy and rated miles are now the growth of each counter from the
  first to the last sample of a session. A null first value counts as
  zero; a counter that goes down inside a session was restarted by the
  car, so the last value is used. The existing positivity checks stay.
- Sessions are cut by charging state. "Charging" and "Starting" samples
  form a run, a "Stopped"/"NoPower" pause shorter than ten minutes does
  not end it, and "Complete", "Disconnected", any other state or a longer
  pause does. Blips no longer inflate the count behind the learning gate.
- ChargeSession and chargeSessions() move from app to core/health so the
  core tests cover them. The file is ChargeSession.kt because detekt's
  MatchingDeclarationName wants a single class to name its file.
- The comment above LEARNING_SESSIONS described the twenty sessions of
  HIGH_SESSIONS; it now describes the three, the other sentence moves to
  HIGH_SESSIONS, and the value stays 3.
- core gets the kotlin.test test dependency that ADR-0007 asks new core
  tests to use.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0129RzhbEE2cboMcYyZMuxqX
@github-actions

github-actions Bot commented Oct 7, 2026

Copy link
Copy Markdown
Contributor

Screenshots

App screenshots: light and dark

Captured from the demo build at 1bd232d.

@dzid26
dzid26 merged commit 486ebc3 into main Oct 7, 2026
4 checks passed
@dzid26
dzid26 deleted the work/health-session-deltas branch October 7, 2026 22:02
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.

Health: capacity and full range are over-estimated when a charge session is first seen mid-way

2 participants