From 508a86b8edeea0c79796bdea0d40d35d65f2fc73 Mon Sep 17 00:00:00 2001 From: MrYoranimo Date: Tue, 16 Apr 2024 15:30:28 +0200 Subject: [PATCH] Xiaomi: fix determining fall asleep time Because the previous implementation of determining the time the user falls asleep in a given time range would take the 24 hours in advance into account, graphs displaying sleep data would erroneously indicate that the user has been asleep since the start of the timeframe if the user was asleep during the rollover of the time frame 24 hours before. This commit change the algorithm to only fetch the last sleep stage sample and sleep range sample from the database that occurred before the given time range. This saves having to process 24 hours worth of samples before the time range in both cases, and prevents taking into account irrelevant sleep ranges. --- .../devices/AbstractTimeSampleProvider.java | 19 ++++ .../devices/xiaomi/XiaomiSampleProvider.java | 92 +++++++++++-------- 2 files changed, 75 insertions(+), 36 deletions(-) diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractTimeSampleProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractTimeSampleProvider.java index 2d9bc1aba..9840e89cb 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractTimeSampleProvider.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractTimeSampleProvider.java @@ -100,6 +100,25 @@ public abstract class AbstractTimeSampleProvider i return samples.get(0); } + public T getLastSampleBefore(final long timestampTo) { + final Device dbDevice = DBHelper.findDevice(getDevice(), getSession()); + if (dbDevice == null) { + // no device, no sample + return null; + } + + final Property deviceIdSampleProp = getDeviceIdentifierSampleProperty(); + final Property timestampSampleProp = getTimestampSampleProperty(); + final List samples = getSampleDao().queryBuilder() + .where(deviceIdSampleProp.eq(dbDevice.getId()), + timestampSampleProp.le(timestampTo)) + .orderDesc(getTimestampSampleProperty()) + .limit(1) + .list(); + + return !samples.isEmpty() ? samples.get(0) : null; + } + @Nullable @Override public T getFirstSample() { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/xiaomi/XiaomiSampleProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/xiaomi/XiaomiSampleProvider.java index 5fd9581b2..5e9a49672 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/xiaomi/XiaomiSampleProvider.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/xiaomi/XiaomiSampleProvider.java @@ -97,60 +97,80 @@ public class XiaomiSampleProvider extends AbstractSampleProvider samples, final int timestamp_from, final int timestamp_to) { final RangeMap stagesMap = new RangeMap<>(); final XiaomiSleepStageSampleProvider sleepStagesSampleProvider = new XiaomiSleepStageSampleProvider(getDevice(), getSession()); - final List stageSamples = sleepStagesSampleProvider.getAllSamples( - timestamp_from * 1000L - 86400000L, + + // Retrieve the last stage before this time range, as the user could have been asleep during + // the range transition + final XiaomiSleepStageSample lastSleepStageBeforeRange = sleepStagesSampleProvider.getLastSampleBefore(timestamp_from * 1000L); + + if (lastSleepStageBeforeRange != null) { + LOG.debug("Last sleep stage before range: ts={}, stage={}", lastSleepStageBeforeRange.getTimestamp(), lastSleepStageBeforeRange.getStage()); + stagesMap.put(lastSleepStageBeforeRange.getTimestamp(), getActivityKindForSample(lastSleepStageBeforeRange)); + } + + // Retrieve all sleep stage samples during the range + final List sleepStagesInRange = sleepStagesSampleProvider.getAllSamples( + timestamp_from * 1000L, timestamp_to * 1000L ); - if (!stageSamples.isEmpty()) { + + if (!sleepStagesInRange.isEmpty()) { // We got actual sleep stages - LOG.debug("Found {} sleep stage samples between {} and {}", stageSamples.size(), timestamp_from, timestamp_to); + LOG.debug("Found {} sleep stage samples between {} and {}", sleepStagesInRange.size(), timestamp_from, timestamp_to); - for (final XiaomiSleepStageSample stageSample : stageSamples) { - final int activityKind; - - switch (stageSample.getStage()) { - case 2: // deep - activityKind = ActivityKind.TYPE_DEEP_SLEEP; - break; - case 3: // light - activityKind = ActivityKind.TYPE_LIGHT_SLEEP; - break; - case 4: // rem - activityKind = ActivityKind.TYPE_REM_SLEEP; - break; - case 0: // final awake - case 1: // ? - case 5: // awake during the night - default: - activityKind = ActivityKind.TYPE_UNKNOWN; - break; - } - stagesMap.put(stageSample.getTimestamp(), activityKind); + for (final XiaomiSleepStageSample stageSample : sleepStagesInRange) { + stagesMap.put(stageSample.getTimestamp(), getActivityKindForSample(stageSample)); } } - // Fetch bed and wakeup times as well. final XiaomiSleepTimeSampleProvider sleepTimeSampleProvider = new XiaomiSleepTimeSampleProvider(getDevice(), getSession()); - final List sleepTimeSamples = sleepTimeSampleProvider.getAllSamples( - timestamp_from * 1000L - 86400000L, + + // Find last sleep sample before the requested range, as the recorded wake up time may be + // in the current range + final XiaomiSleepTimeSample lastSleepTimesBeforeRange = sleepTimeSampleProvider.getLastSampleBefore(timestamp_from * 1000L); + + if (lastSleepTimesBeforeRange != null) { + stagesMap.put(lastSleepTimesBeforeRange.getWakeupTime(), ActivityKind.TYPE_UNKNOWN); + stagesMap.put(lastSleepTimesBeforeRange.getTimestamp(), ActivityKind.TYPE_LIGHT_SLEEP); + } + + // Find all wake up and sleep samples in the current time range + final List sleepTimesInRange = sleepTimeSampleProvider.getAllSamples( + timestamp_from * 1000L, timestamp_to * 1000L ); - if (!sleepTimeSamples.isEmpty()) { - LOG.debug("Found {} sleep samples between {} and {}", sleepTimeSamples.size(), timestamp_from, timestamp_to); - for (final XiaomiSleepTimeSample stageSample : sleepTimeSamples) { - if (stageSamples.isEmpty()) { + + if (!sleepTimesInRange.isEmpty()) { + LOG.debug("Found {} sleep samples between {} and {}", sleepTimesInRange.size(), timestamp_from, timestamp_to); + for (final XiaomiSleepTimeSample stageSample : sleepTimesInRange) { + if (sleepStagesInRange.isEmpty()) { // Only overlay them as light sleep if we don't have actual sleep stages stagesMap.put(stageSample.getTimestamp(), ActivityKind.TYPE_LIGHT_SLEEP); }