From 290379a462d5bb5aa485ca900052b3f90a349070 Mon Sep 17 00:00:00 2001 From: Gideon Zenz Date: Wed, 19 Nov 2025 19:41:51 +0100 Subject: [PATCH] - Use SleepAnalysis for last night HRV calculation - remove ColmiHrvSummarySampleProvider --- .../ComputedHrvSummarySampleProvider.java | 112 ++++++++++++++---- .../devices/huami/HuamiCoordinator.java | 2 +- .../UltrahumanDeviceCoordinator.java | 2 +- .../ring/AbstractYawellRingCoordinator.java | 2 +- .../ColmiHrvSummarySampleProvider.java | 56 --------- 5 files changed, 94 insertions(+), 80 deletions(-) delete mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/yawell/ring/samples/ColmiHrvSummarySampleProvider.java diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/ComputedHrvSummarySampleProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/ComputedHrvSummarySampleProvider.java index 7f9043755b..01beb69811 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/ComputedHrvSummarySampleProvider.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/ComputedHrvSummarySampleProvider.java @@ -26,6 +26,10 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import nodomain.freeyourgadget.gadgetbridge.activities.charts.SleepAnalysis; +import nodomain.freeyourgadget.gadgetbridge.entities.AbstractActivitySample; +import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.model.HrvSummarySample; import nodomain.freeyourgadget.gadgetbridge.model.HrvValueSample; @@ -36,7 +40,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.HrvValueSample; *

* The provider calculates: * - Weekly average (7-day rolling average of all HRV values) - * - Last night average and 5-min high (8 hours before midnight to midnight) + * - Last night average and 5-min high (based on actual sleep sessions detected from activity data) * - Baseline values using Garmin's method: mean ± standard deviation of 28 overnight averages *

* Summaries are computed on-demand and cached for performance. @@ -47,6 +51,8 @@ public class ComputedHrvSummarySampleProvider implements TimeSampleProvider valueProvider; + private final GBDevice device; + private final DaoSession session; // LRU cache for computed summaries private final Map summaryCache = new LinkedHashMap<>(CACHE_SIZE, 0.75f, true) { @@ -56,8 +62,12 @@ public class ComputedHrvSummarySampleProvider implements TimeSampleProvider valueProvider) { + public ComputedHrvSummarySampleProvider(final TimeSampleProvider valueProvider, + final GBDevice device, + final DaoSession session) { this.valueProvider = valueProvider; + this.device = device; + this.session = session; } @NonNull @@ -133,12 +143,50 @@ public class ComputedHrvSummarySampleProvider implements TimeSampleProvider sampleProvider = + device.getDeviceCoordinator().getSampleProvider(device, session); + final List activitySamples = + sampleProvider.getAllActivitySamples(startTs, endTs); + + if (!activitySamples.isEmpty()) { + final SleepAnalysis sleepAnalysis = new SleepAnalysis(); + final List sleepSessions = + sleepAnalysis.calculateSleepSessions(activitySamples); + + // Use the last (most recent) sleep session - this should be the previous night + // SleepAnalysis returns sessions with their actual start/end times + if (!sleepSessions.isEmpty()) { + final SleepAnalysis.SleepSession lastSession = sleepSessions.get(sleepSessions.size() - 1); + final long sessionStart = lastSession.getSleepStart().getTime(); + final long sessionEnd = lastSession.getSleepEnd().getTime(); + + lastNightAvg = calculateAverageHrv(sessionStart, sessionEnd); + lastNight5MinHigh = calculate5MinHighHrv(sessionStart, sessionEnd); + } + } // Calculate baseline values (using past 28 overnight averages) final BaselineValues baseline = calculateBaseline(dayEndTimestamp); @@ -206,30 +254,52 @@ public class ComputedHrvSummarySampleProvider implements TimeSampleProvider sampleProvider = + device.getDeviceCoordinator().getSampleProvider(device, session); + final SleepAnalysis sleepAnalysis = new SleepAnalysis(); + // Collect overnight averages for the past 28 days for (int i = 0; i < DAYS_FOR_BASELINE; i++) { - // Define "overnight" as 8 hours before midnight to midnight - final Calendar dayStart = (Calendar) cal.clone(); - dayStart.set(Calendar.HOUR_OF_DAY, 0); - dayStart.set(Calendar.MINUTE, 0); - dayStart.set(Calendar.SECOND, 0); - dayStart.set(Calendar.MILLISECOND, 0); + // Get activity samples for a 24-hour window (noon to noon) to find sleep sessions + final Calendar searchEnd = (Calendar) cal.clone(); + searchEnd.set(Calendar.HOUR_OF_DAY, 12); + searchEnd.set(Calendar.MINUTE, 0); + searchEnd.set(Calendar.SECOND, 0); + searchEnd.set(Calendar.MILLISECOND, 0); - final Calendar nightStart = (Calendar) dayStart.clone(); - nightStart.add(Calendar.HOUR_OF_DAY, -8); + final Calendar searchStart = (Calendar) searchEnd.clone(); + searchStart.add(Calendar.DATE, -1); - final int nightAvg = calculateAverageHrv(nightStart.getTimeInMillis(), dayStart.getTimeInMillis()); - if (nightAvg > 0) { - overnightAverages.add(nightAvg); + final int startTs = (int) (searchStart.getTimeInMillis() / 1000); + final int endTs = (int) (searchEnd.getTimeInMillis() / 1000); + + final List activitySamples = + sampleProvider.getAllActivitySamples(startTs, endTs); + + if (!activitySamples.isEmpty()) { + final List sleepSessions = + sleepAnalysis.calculateSleepSessions(activitySamples); + + // Use the last sleep session in the window (should be the previous night) + if (!sleepSessions.isEmpty()) { + final SleepAnalysis.SleepSession lastSession = sleepSessions.get(sleepSessions.size() - 1); + final long sessionStart = lastSession.getSleepStart().getTime(); + final long sessionEnd = lastSession.getSleepEnd().getTime(); + + final int nightAvg = calculateAverageHrv(sessionStart, sessionEnd); + if (nightAvg > 0) { + overnightAverages.add(nightAvg); + } + } } // Move to previous day cal.add(Calendar.DATE, -1); } - // Require at least 7 days of overnight data before calculating a baseline + // Require at least DAYS_FOR_WEEKLY_AVG of overnight data before calculating a baseline // This prevents unreliable status calculations in the first few days - if (overnightAverages.size() < 7) { + if (overnightAverages.size() < DAYS_FOR_WEEKLY_AVG) { return new BaselineValues(0, 0, 0); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/HuamiCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/HuamiCoordinator.java index d65cd01c4f..ab15ce8f98 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/HuamiCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/HuamiCoordinator.java @@ -212,7 +212,7 @@ public abstract class HuamiCoordinator extends AbstractBLEDeviceCoordinator { @Override public TimeSampleProvider getHrvSummarySampleProvider(GBDevice device, DaoSession session) { - return new ComputedHrvSummarySampleProvider(getHrvValueSampleProvider(device, session)); + return new ComputedHrvSummarySampleProvider(getHrvValueSampleProvider(device, session), device, session); } @Override diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/ultrahuman/UltrahumanDeviceCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/ultrahuman/UltrahumanDeviceCoordinator.java index 29d46c4872..45278797b4 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/ultrahuman/UltrahumanDeviceCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/ultrahuman/UltrahumanDeviceCoordinator.java @@ -154,7 +154,7 @@ public class UltrahumanDeviceCoordinator extends AbstractBLEDeviceCoordinator { @Override public TimeSampleProvider getHrvSummarySampleProvider(GBDevice device, DaoSession session) { - return new ComputedHrvSummarySampleProvider(getHrvValueSampleProvider(device, session)); + return new ComputedHrvSummarySampleProvider(getHrvValueSampleProvider(device, session), device, session); } @Override diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/yawell/ring/AbstractYawellRingCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/yawell/ring/AbstractYawellRingCoordinator.java index 9a11451ff2..b951344ef7 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/yawell/ring/AbstractYawellRingCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/yawell/ring/AbstractYawellRingCoordinator.java @@ -179,7 +179,7 @@ public abstract class AbstractYawellRingCoordinator extends AbstractBLEDeviceCoo @Override public TimeSampleProvider getHrvSummarySampleProvider(GBDevice device, DaoSession session) { - return new ComputedHrvSummarySampleProvider(getHrvValueSampleProvider(device, session)); + return new ComputedHrvSummarySampleProvider(getHrvValueSampleProvider(device, session), device, session); } @Override diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/yawell/ring/samples/ColmiHrvSummarySampleProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/yawell/ring/samples/ColmiHrvSummarySampleProvider.java deleted file mode 100644 index 89d6ff5219..0000000000 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/yawell/ring/samples/ColmiHrvSummarySampleProvider.java +++ /dev/null @@ -1,56 +0,0 @@ -/* Copyright (C) 2024 Arjan Schrijver - - This file is part of Gadgetbridge. - - Gadgetbridge is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - Gadgetbridge is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . */ -package nodomain.freeyourgadget.gadgetbridge.devices.yawell.ring.samples; - -import androidx.annotation.NonNull; - -import de.greenrobot.dao.AbstractDao; -import de.greenrobot.dao.Property; -import nodomain.freeyourgadget.gadgetbridge.devices.AbstractTimeSampleProvider; -import nodomain.freeyourgadget.gadgetbridge.entities.ColmiHrvSummarySample; -import nodomain.freeyourgadget.gadgetbridge.entities.ColmiHrvSummarySampleDao; -import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; -import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; - -public class ColmiHrvSummarySampleProvider extends AbstractTimeSampleProvider { - public ColmiHrvSummarySampleProvider(final GBDevice device, final DaoSession session) { - super(device, session); - } - - @NonNull - @Override - public AbstractDao getSampleDao() { - return getSession().getColmiHrvSummarySampleDao(); - } - - @NonNull - @Override - protected Property getTimestampSampleProperty() { - return ColmiHrvSummarySampleDao.Properties.Timestamp; - } - - @NonNull - @Override - protected Property getDeviceIdentifierSampleProperty() { - return ColmiHrvSummarySampleDao.Properties.DeviceId; - } - - @Override - public ColmiHrvSummarySample createSample() { - return new ColmiHrvSummarySample(); - } -}