fix(segments): recharge detection for devices that sleep through charging - #307
Merged
Conversation
…rough charging The recharge detector smoothed SoC over 11 samples, not 11 minutes. Aftermarket devices (Ruptela, AutoPi, Macaron, most OEM connections) report every 1-3 s while awake and nothing while asleep, so the window straddled sleep gaps, pulled the trough and peak into the drives on either side, and the odometer rule rejected the session. Vehicle 192097 (Ruptela R1) lost 3 overnight charges in 31 days and reported a 90-second sensor glitch as a session. Replace smoothing with a pure detector over raw samples: - monotone runs tolerate dips of 1.0 (integer staircase, float jitter); the peak is the first sample reaching the maximum so a run never bleeds into departure - the start reading is the last SoC at or before the car last moved (odometer walk-back within 0.5 km), bounded by the trough, skipping the flat lead-in - a rise must exceed the run tolerance and stay under 600 %/h - sessions within 2 h with unchanged odometer still merge Add Segment.maxSampleGapSeconds (recharge only): the longest interval with no SoC sample, so clients can tell how much of the duration was unobserved. Add a local real-data harness (RECHARGE_REALDATA_JSON) that skips when unset.
…al window Review findings on the first revision: - a charge followed by a short hop to a second charger with no SoC drop was folded into the second session (twice on 192097 in one month); the part of a run before the last movement is now evaluated on its own - a sparse odometer (Tesla, 5 min) lagged the start by 2-8 SoC points; the start reading is now the lowest SoC between the last moving and the first stationary odometer sample - merge compares the odometer at each peak within epsilon instead of exact equality at boundary samples that two sessions may share - odometer walk-back uses the absolute step so a backwards glitch does not count as stationary; cheap raw-rise check before the walk-back - run make generate so the MCP condensed schema carries the new field - document the [from, to] boundary and the monotone-continuation behaviour
Loads a local SoC/odometer export (RECHARGE_REALDATA_JSON, skipped when unset, never committed) into the ClickHouse container and exercises JWT auth, the segments resolver, repository summary signals, config overrides, pagination and the MCP telemetry_get_trip_segments tool.
after+1ns is truncated to microseconds in the ClickHouse bound, so the segment starting exactly at the cursor was returned again on the next page for every mechanism. Drop segments with start <= after before applying the limit.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
segments(mechanism: recharge)smoothed SoC over 11 samples, not 11 minutes. Aftermarket devices (Ruptela, AutoPi, Macaron, most OEM connections) report every 1–3 s while awake and nothing while asleep, so the window straddled sleep gaps, pulled the trough/peak into the drives on either side, and the odometer rule rejected the session. The mobile app's Charging habits card depends on these segments for vehicles that have SoC but no power/energy signals.Evidence from vehicle 192097 (2025 VW ID. Buzz, Ruptela R1), Aug 12 – Sep 11 2026:
maxSampleGapSeconds= 63,000Note on Sep 1: the 1-minute export shows SoC 22 at 16:15–16:17 while the odometer still ticked, so 22 (not the 23 read at 15:47) is the last reading before parking at the second spot. The charge then continues monotonically to 76 on Sep 2 with the odometer unchanged, so it is one plug-in at one location, reported as one stint with the unobserved portion in
maxSampleGapSeconds.Design (one regime-aware
recharge, no new mechanism)minIncreasePercent(default 1, honoured fromSegmentConfig) and > the 1.0 run tolerance (a one-point change on integer OBD is quantization flicker: 46,46,47,46 showed up three times on 192097). Rate cap 600 %/h drops sensor glitches.maxSampleGapSecondsis computed over the merged range.detectRechargeSessionsis a pure function over SoC/odometer slices;DetectSegmentsonly queries and calls it.smoothSamples,findTroughToPeakRanges,filterRangesBySocAndOdoand their tests are gone.Schema
type SegmentgainsmaxSampleGapSeconds: Int(nullable): the longest interval inside the segment with no SoC sample. Set for recharge, null elsewhere. Therechargedoc string now says duration is an upper bound for devices that sleep through a charge, that a session is only reported when both bounding readings fall inside[from, to], and that consecutive charges at the same odometer with no drop are one session.make generatewas run; the gqlgen and MCP diffs are limited to the new field and doc string.Also in this PR
Exclusive
aftercursor fix (internal/repositories/segments.go, all mechanisms, pre-existing):after + 1nsis truncated to microseconds in the ClickHouse bound, so the segment starting exactly at the cursor was returned again on the next page. Found by the end-to-end pagination check below; segments withstart <= afterare now dropped before the limit is applied.Verification
go test ./...(incl. the e2eRechargeDetectorcase against ClickHouse),go vet ./...,make lint: clean.internal/service/ch/recharge_realdata_test.go,RECHARGE_REALDATA_JSON, skips when unset; nothing committed) against 1-minute LAST exports.e2e/recharge_realdata_test.go, same env var): loads the export into the ClickHouse container and runs the real HTTP stack: JWT auth,segmentsresolver, repository summary FIRST/LAST signals,minIncreasePercentoverride,limit/afterpagination,maxSampleGapSecondsnull onfrequencyAnalysisandrefuelempty, and the MCPtelemetry_get_trip_segmentstool returning the same session count as GraphQL.An independent review pass (code + harness) was run on the first revision; its findings (hop between charges, sparse odometer lag, stale MCP schema, range-boundary docs) are addressed in the second commit.
Follow-ups (not in this PR)
LASTSoC can read one or two points below the peak. The repositories layer extends the summary window 2 minutes pastendfor refuel/recharge (sized for the old lagging peak). Withendnow at the first sample reaching the max, a car that departs within 2 minutes contributes a lower post-peak reading: 1 of 26 Ruptela sessions (Sep 3 22:45, 18→76 vs peak 78) and 3 of 14 Tesla sessions (one point low). Dropping the buffer for recharge is a one-line change ininternal/repositories/segments.gobut touches the summary computation, which was out of scope here.fromor end aftertoare invisible because SoC/odometer are only loaded for[from, to); a sleeping device needs both bounding readings inside the window. A ±48 h lookback/lookahead with clipping andstartedBeforeRangewould close that.