Repository navigation
Export confirmed naps to Apple Health (iOS) as daytime sleep segments + 3 h nap cap #677
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
e8a7afa
1fb0e12
28e42c6
7cebb76
c3777e9
025b589
d1438d3
a49d891
22b9843
2a23ce3
2956770
a33bd35
02a7677
1d861e4
a386b20
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1852,7 +1852,12 @@ import 'vendor_sleep.dart'; | |
| // more than the windowed live/phone sources, instead of only when they counted | ||
| // nothing, so a short tracked walk no longer hides the day's chip total. | ||
| // gen4 (no counter) is unchanged. Edge-only. | ||
| const int kAlgoVersion = 120; | ||
| // 120 → 121 (nap cap 3 h): naps are capped at kMaxNapSec on every path — | ||
| // detected bouts via dropOversizedNaps AND stored edits replayed through | ||
| // applyNapEdits — so a pre-cap manual nap longer than 3 h no longer counts | ||
| // into naps/nap_min. Days derived at v120 keep their stale naps without this | ||
| // bump. | ||
| const int kAlgoVersion = 121; | ||
| /// The sibling SHAs this version was derived against, asserted against | ||
| /// pubspec.yaml in test/db_serve_version_and_reads_test.dart. | ||
| /// | ||
|
|
@@ -10082,7 +10087,9 @@ class DerivationEngine { | |
| List<NapEdit> napEdits, | ||
| String note, | ||
| ) { | ||
| final merged = applyNapEdits(const [], napEdits); | ||
| // Cap stored edits too: a manual nap logged before the 3 h cap must not | ||
| // survive a re-derive as a counted nap. | ||
| final merged = dropOversizedNaps(applyNapEdits(const [], napEdits)); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| if (merged.isEmpty) { | ||
| _writeUnknownNaps(bundle, note); | ||
| return null; | ||
|
|
@@ -10269,9 +10276,17 @@ class DerivationEngine { | |
| 'confidence': nap.confidence, | ||
| }, | ||
| ]; | ||
| candidates?.addAll(detected.where((n) => !napEdits.any((e) => | ||
| // A nap longer than kMaxNapSec (3 h) is a sleep, not a nap — drop it | ||
| // before it can reach the suggestions, `nap_min`, sleep need or the | ||
| // timeline. Entry-time paths share the same constant via | ||
| // manualNapWindowIsValid; stored edits are capped again at the merge | ||
| // below so pre-cap rows cannot survive a re-derive. | ||
| final capped = dropOversizedNaps(detected); | ||
|
sourcery-ai[bot] marked this conversation as resolved.
Resolved
sourcery-ai[bot] marked this conversation as resolved.
Resolved
|
||
| candidates?.addAll(capped.where((n) => !napEdits.any((e) => | ||
|
Comment on lines
+10284
to
+10285
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| e.overlaps(n['start'] as int, n['end'] as int)))); | ||
| final merged = applyNapEdits(const [], napEdits); | ||
| // Cap stored edits too: a manual nap logged before the 3 h cap must not | ||
| // survive a re-derive as a counted nap. | ||
| final merged = dropOversizedNaps(applyNapEdits(const [], napEdits)); | ||
|
|
||
| bundle['naps'] = <String, dynamic>{ | ||
| 'value': merged, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -54,9 +54,14 @@ typedef NapMap = Map<String, dynamic>; | |
| /// scatter of two-minute entries would swamp the real ones in every total. | ||
| const int kMinManualNapSec = 5 * 60; | ||
|
|
||
| /// Maximum. Anything longer is a sleep, not a nap, and belongs in the main | ||
| /// sleep window where the stager can actually say something about its stages. | ||
| const int kMaxManualNapSec = 6 * 60 * 60; | ||
| /// Maximum length of anything counted as a nap: 3 hours. Anything longer is a | ||
| /// sleep, not a nap, and belongs in the main sleep window where the stager can | ||
| /// actually say something about its stages. Applies to detected AND manually | ||
| /// logged naps alike — one cap, one place. | ||
| const int kMaxNapSec = 3 * 60 * 60; | ||
|
|
||
| /// Maximum for a manually logged nap — the same cap as detected naps. | ||
| const int kMaxManualNapSec = kMaxNapSec; | ||
|
Comment on lines
+63
to
+64
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
|
||
| /// Whether a window can be logged as a nap. | ||
| bool manualNapWindowIsValid(int startSec, int endSec) { | ||
|
|
@@ -129,6 +134,16 @@ List<NapMap> applyNapEdits(List<NapMap> detected, List<NapEdit> edits) { | |
| return out; | ||
| } | ||
|
|
||
| /// Drops every nap whose window exceeds [kMaxNapSec]. A detected bout longer | ||
| /// than 3 h is a sleep fragment the detector misfiled as a nap; crediting it | ||
| /// into `nap_min` would count a night's sleep as daytime rest. The filter is | ||
| /// pure so the rule is testable without a derivation run. | ||
| List<NapMap> dropOversizedNaps(List<NapMap> naps) => [ | ||
| for (final n in naps) | ||
| if ((n['end'] as num).toInt() - (n['start'] as num).toInt() <= kMaxNapSec) | ||
| n, | ||
| ]; | ||
|
|
||
| /// Total asleep minutes across [naps] — the `nap_min` scalar. | ||
| /// | ||
| /// Always a number, including 0 for an empty list. The absent-versus-zero | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟡 Medium · Overlapping naps erase each other
When two confirmed or manual nap rows have distinct start times but overlapping windows,
deleteAndSavedeletes every app-owned sleep sample overlapping the nap window before saving only the current nap. The nap rows can overlap becausesleep_naponly keys uniqueness by(day_id, start_ts), andputNapEditchecks overlap with main sleep but not with other naps; exporting either overlapping row therefore removes the other nap’s samples from Apple Health.Prevent overlapping nap rows when writing them, or give each exported nap a stable identity and delete only that nap’s own samples.
Prompt for AI agents