From c52fd53ebdd2902bcd4039cbc53aa4b154123d28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Rebelo?= Date: Sun, 24 Nov 2024 19:44:18 +0000 Subject: [PATCH] Garmin: Persist sleep score --- .../gadgetbridge/daogen/GBDaoGenerator.java | 11 +- .../devices/AbstractDeviceCoordinator.java | 11 + .../DefaultRestingMetabolicRateProvider.java | 12 + .../devices/DeviceCoordinator.java | 8 + .../devices/garmin/GarminCoordinator.java | 11 + .../GarminSleepStatsSampleProvider.java | 56 +++ .../huawei/HuaweiSpo2SampleProvider.java | 11 + .../entities/AbstractTimeSample.java | 4 + .../gadgetbridge/model/SleepScoreSample.java | 37 ++ .../devices/garmin/fit/FitImporter.java | 362 ++++-------------- .../devices/garmin/fit/GlobalFITMessage.java | 17 + .../garmin/fit/messages/FitSleepStats.java | 85 ++++ 12 files changed, 338 insertions(+), 287 deletions(-) create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/GarminSleepStatsSampleProvider.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/SleepScoreSample.java diff --git a/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java b/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java index 8ab32911a..86d111b78 100644 --- a/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java +++ b/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java @@ -54,7 +54,7 @@ public class GBDaoGenerator { public static void main(String[] args) throws Exception { - final Schema schema = new Schema(87, MAIN_PACKAGE + ".entities"); + final Schema schema = new Schema(88, MAIN_PACKAGE + ".entities"); Entity userAttributes = addUserAttributes(schema); Entity user = addUserInfo(schema, userAttributes); @@ -128,6 +128,7 @@ public class GBDaoGenerator { addGarminRespiratoryRateSample(schema, user, device); addGarminHeartRateRestingSample(schema, user, device); addGarminRestingMetabolicRateSample(schema, user, device); + addGarminSleepStatsSample(schema, user, device); addPendingFile(schema, user, device); addWena3EnergySample(schema, user, device); addWena3BehaviorSample(schema, user, device); @@ -879,6 +880,14 @@ public class GBDaoGenerator { return sample; } + private static Entity addGarminSleepStatsSample(Schema schema, Entity user, Entity device) { + Entity sample = addEntity(schema, "GarminSleepStatsSample"); + sample.addImport(MAIN_PACKAGE + ".model.SleepScoreSample"); + addCommonTimeSampleProperties("SleepScoreSample", sample, user, device); + sample.addIntProperty("sleepScore").notNull().codeBeforeGetter(OVERRIDE); + return sample; + } + private static Entity addPendingFile(Schema schema, Entity user, Entity device) { Entity pendingFile = addEntity(schema, "PendingFile"); pendingFile.setJavaDoc( diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractDeviceCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractDeviceCoordinator.java index 069318853..732690eef 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractDeviceCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractDeviceCoordinator.java @@ -78,6 +78,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.HrvValueSample; import nodomain.freeyourgadget.gadgetbridge.model.PaiSample; import nodomain.freeyourgadget.gadgetbridge.model.RespiratoryRateSample; import nodomain.freeyourgadget.gadgetbridge.model.RestingMetabolicRateSample; +import nodomain.freeyourgadget.gadgetbridge.model.SleepScoreSample; import nodomain.freeyourgadget.gadgetbridge.model.Spo2Sample; import nodomain.freeyourgadget.gadgetbridge.model.StressSample; import nodomain.freeyourgadget.gadgetbridge.model.TemperatureSample; @@ -286,6 +287,11 @@ public abstract class AbstractDeviceCoordinator implements DeviceCoordinator { return new DefaultRestingMetabolicRateProvider(device, session); } + @Override + public TimeSampleProvider getSleepScoreProvider(final GBDevice device, final DaoSession session) { + return null; + } + @Override @Nullable public ActivitySummaryParser getActivitySummaryParser(final GBDevice device, final Context context) { @@ -684,6 +690,11 @@ public abstract class AbstractDeviceCoordinator implements DeviceCoordinator { return false; } + @Override + public boolean supportsSleepScore() { + return false; + } + @Override public boolean supportsWeather() { return false; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DefaultRestingMetabolicRateProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DefaultRestingMetabolicRateProvider.java index e97e55061..756d786cc 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DefaultRestingMetabolicRateProvider.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DefaultRestingMetabolicRateProvider.java @@ -25,6 +25,8 @@ import java.util.List; import de.greenrobot.dao.AbstractDao; import de.greenrobot.dao.Property; import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; +import nodomain.freeyourgadget.gadgetbridge.entities.Device; +import nodomain.freeyourgadget.gadgetbridge.entities.User; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser; import nodomain.freeyourgadget.gadgetbridge.model.RestingMetabolicRateSample; @@ -170,6 +172,16 @@ public class DefaultRestingMetabolicRateProvider extends AbstractTimeSampleProvi } + @Override + public void setDevice(final Device device) { + + } + + @Override + public void setUser(final User user) { + + } + @Override public long getTimestamp() { return timestamp; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceCoordinator.java index dd7b224d3..6af9b1a9d 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceCoordinator.java @@ -58,6 +58,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.HrvValueSample; import nodomain.freeyourgadget.gadgetbridge.model.PaiSample; import nodomain.freeyourgadget.gadgetbridge.model.RespiratoryRateSample; import nodomain.freeyourgadget.gadgetbridge.model.RestingMetabolicRateSample; +import nodomain.freeyourgadget.gadgetbridge.model.SleepScoreSample; import nodomain.freeyourgadget.gadgetbridge.model.Spo2Sample; import nodomain.freeyourgadget.gadgetbridge.model.StressSample; import nodomain.freeyourgadget.gadgetbridge.model.TemperatureSample; @@ -376,6 +377,8 @@ public interface DeviceCoordinator { TimeSampleProvider getRestingMetabolicRateProvider(GBDevice device, DaoSession session); + TimeSampleProvider getSleepScoreProvider(GBDevice device, DaoSession session); + /** * Returns the {@link ActivitySummaryParser} for the device being supported. * @@ -574,6 +577,11 @@ public interface DeviceCoordinator { */ boolean supportsAwakeSleep(); + /** + * Indicates whether the device supports determining a sleep score in a 0-100 range. + */ + boolean supportsSleepScore(); + /** * Indicates whether the device supports current weather and/or weather * forecast display. diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/GarminCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/GarminCoordinator.java index 8df8782a6..4ac4bc5b9 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/GarminCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/GarminCoordinator.java @@ -46,6 +46,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.HrvSummarySample; import nodomain.freeyourgadget.gadgetbridge.model.HrvValueSample; import nodomain.freeyourgadget.gadgetbridge.model.RespiratoryRateSample; import nodomain.freeyourgadget.gadgetbridge.model.RestingMetabolicRateSample; +import nodomain.freeyourgadget.gadgetbridge.model.SleepScoreSample; import nodomain.freeyourgadget.gadgetbridge.model.Spo2Sample; import nodomain.freeyourgadget.gadgetbridge.model.StressSample; import nodomain.freeyourgadget.gadgetbridge.model.Vo2MaxSample; @@ -159,6 +160,11 @@ public abstract class GarminCoordinator extends AbstractBLEDeviceCoordinator { return new GarminRestingMetabolicRateSampleProvider(device, session); } + @Override + public TimeSampleProvider getSleepScoreProvider(final GBDevice device, final DaoSession session) { + return new GarminSleepStatsSampleProvider(device, session); + } + @Override public GarminHeartRateRestingSampleProvider getHeartRateRestingSampleProvider(final GBDevice device, final DaoSession session) { return new GarminHeartRateRestingSampleProvider(device, session); @@ -294,6 +300,11 @@ public abstract class GarminCoordinator extends AbstractBLEDeviceCoordinator { return true; } + @Override + public boolean supportsSleepScore() { + return true; + } + @Override public boolean supportsRespiratoryRate() { return true; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/GarminSleepStatsSampleProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/GarminSleepStatsSampleProvider.java new file mode 100644 index 000000000..607a0d70b --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/GarminSleepStatsSampleProvider.java @@ -0,0 +1,56 @@ +/* Copyright (C) 2024 José Rebelo + + 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.garmin; + +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.DaoSession; +import nodomain.freeyourgadget.gadgetbridge.entities.GarminSleepStatsSample; +import nodomain.freeyourgadget.gadgetbridge.entities.GarminSleepStatsSampleDao; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; + +public class GarminSleepStatsSampleProvider extends AbstractTimeSampleProvider { + public GarminSleepStatsSampleProvider(final GBDevice device, final DaoSession session) { + super(device, session); + } + + @NonNull + @Override + public AbstractDao getSampleDao() { + return getSession().getGarminSleepStatsSampleDao(); + } + + @NonNull + @Override + protected Property getTimestampSampleProperty() { + return GarminSleepStatsSampleDao.Properties.Timestamp; + } + + @NonNull + @Override + protected Property getDeviceIdentifierSampleProperty() { + return GarminSleepStatsSampleDao.Properties.DeviceId; + } + + @Override + public GarminSleepStatsSample createSample() { + return new GarminSleepStatsSample(); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiSpo2SampleProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiSpo2SampleProvider.java index 4c7ce80f3..62c5d5c4b 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiSpo2SampleProvider.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiSpo2SampleProvider.java @@ -35,6 +35,7 @@ import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; import nodomain.freeyourgadget.gadgetbridge.entities.Device; import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiActivitySample; import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiActivitySampleDao; +import nodomain.freeyourgadget.gadgetbridge.entities.User; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; public class HuaweiSpo2SampleProvider extends AbstractTimeSampleProvider { @@ -206,6 +207,16 @@ public class HuaweiSpo2SampleProvider extends AbstractTimeSampleProvider. */ +package nodomain.freeyourgadget.gadgetbridge.model; + +import androidx.annotation.NonNull; + +import nodomain.freeyourgadget.gadgetbridge.entities.AbstractTimeSample; +import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils; + +public abstract class SleepScoreSample extends AbstractTimeSample { + public abstract int getSleepScore(); + + @NonNull + @Override + public String toString() { + return getClass().getSimpleName() + "{" + + "timestamp=" + DateTimeUtils.formatDateTime(DateTimeUtils.parseTimestampMillis(getTimestamp())) + + ", userId=" + getUserId() + + ", deviceId=" + getDeviceId() + + ", sleepScore=" + getSleepScore() + + "}"; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/FitImporter.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/FitImporter.java index 2f97dbf34..3ea2ba90e 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/FitImporter.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/FitImporter.java @@ -19,6 +19,7 @@ import java.util.TreeMap; import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; import nodomain.freeyourgadget.gadgetbridge.database.DBHelper; +import nodomain.freeyourgadget.gadgetbridge.devices.AbstractTimeSampleProvider; import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminActivitySampleProvider; import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminBodyEnergySampleProvider; import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminEventSampleProvider; @@ -27,10 +28,12 @@ import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminHrvSummarySampl import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminHrvValueSampleProvider; import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminRespiratoryRateSampleProvider; import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminRestingMetabolicRateSampleProvider; +import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminSleepStatsSampleProvider; import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminSleepStageSampleProvider; import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminSpo2SampleProvider; import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminStressSampleProvider; import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminWorkoutParser; +import nodomain.freeyourgadget.gadgetbridge.entities.AbstractTimeSample; import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary; import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; import nodomain.freeyourgadget.gadgetbridge.entities.Device; @@ -43,6 +46,7 @@ import nodomain.freeyourgadget.gadgetbridge.entities.GarminHrvSummarySample; import nodomain.freeyourgadget.gadgetbridge.entities.GarminHrvValueSample; import nodomain.freeyourgadget.gadgetbridge.entities.GarminRespiratoryRateSample; import nodomain.freeyourgadget.gadgetbridge.entities.GarminSleepStageSample; +import nodomain.freeyourgadget.gadgetbridge.entities.GarminSleepStatsSample; import nodomain.freeyourgadget.gadgetbridge.entities.GarminSpo2Sample; import nodomain.freeyourgadget.gadgetbridge.entities.GarminStressSample; import nodomain.freeyourgadget.gadgetbridge.entities.User; @@ -50,7 +54,6 @@ import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind; import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample; import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser; -import nodomain.freeyourgadget.gadgetbridge.model.RestingMetabolicRateSample; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.FileType; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionHrvStatus; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionSleepStage; @@ -68,6 +71,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages. import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitSleepDataInfo; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitSleepDataRaw; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitSleepStage; +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitSleepStats; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitSpo2; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitSport; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitStressLevel; @@ -87,6 +91,7 @@ public class FitImporter { private final List respiratoryRateSamples = new ArrayList<>(); private final List restingHrSamples = new ArrayList<>(); private final List events = new ArrayList<>(); + private final List sleepStatsSamples = new ArrayList<>(); private final List sleepStageSamples = new ArrayList<>(); private final List hrvSummarySamples = new ArrayList<>(); private final List hrvValueSamples = new ArrayList<>(); @@ -162,6 +167,16 @@ public class FitImporter { final FitSleepDataRaw fitSleepDataRaw = (FitSleepDataRaw) record; //LOG.debug("Sleep Data Raw: {}", fitSleepDataRaw); fitSleepDataRawSamples.add(fitSleepDataRaw); + } else if (record instanceof FitSleepStats) { + final Integer score = ((FitSleepStats) record).getOverallSleepScore(); + if (score == null) { + continue; + } + LOG.trace("Sleep stats at {}: {}", ts, record); + final GarminSleepStatsSample sample = new GarminSleepStatsSample(); + sample.setTimestamp(ts * 1000L); + sample.setSleepScore(score); + sleepStatsSamples.add(sample); } else if (record instanceof FitSleepStage) { final FieldDefinitionSleepStage.SleepStage stage = ((FitSleepStage) record).getSleepStage(); if (stage == null) { @@ -312,30 +327,45 @@ public class FitImporter { return; } - switch (fileId.getType()) { - case ACTIVITY: - persistWorkout(file); - break; - case MONITOR: - persistActivitySamples(); - persistSpo2Samples(); - persistRespiratoryRateSamples(); - persistRestingHrSamples(); - persistStressSamples(); - persistBodyEnergySamples(); - persistRestingMetabolicRateSamples(); - break; - case SLEEP: - persistEvents(); - persistSleepStageSamples(); - processRawSleepSamples(); - break; - case HRV_STATUS: - persistHrvSummarySamples(); - persistHrvValueSamples(); - break; - default: - LOG.warn("Unable to handle fit file of type {}", fileId.getType()); + try (DBHandler handler = GBApplication.acquireDB()) { + final DaoSession session = handler.getDaoSession(); + + switch (fileId.getType()) { + case ACTIVITY: + persistWorkout(file, session); + break; + case MONITOR: + persistActivitySamples(session); + persistAbstractSamples(spo2samples, new GarminSpo2SampleProvider(gbDevice, session)); + persistAbstractSamples(respiratoryRateSamples, new GarminRespiratoryRateSampleProvider(gbDevice, session)); + persistAbstractSamples(restingHrSamples, new GarminHeartRateRestingSampleProvider(gbDevice, session)); + persistAbstractSamples(stressSamples, new GarminStressSampleProvider(gbDevice, session)); + persistAbstractSamples(bodyEnergySamples, new GarminBodyEnergySampleProvider(gbDevice, session)); + persistAbstractSamples(restingMetabolicRateSamples, new GarminRestingMetabolicRateSampleProvider(gbDevice, session)); + break; + case SLEEP: + persistAbstractSamples(events, new GarminEventSampleProvider(gbDevice, session)); + persistAbstractSamples(sleepStatsSamples, new GarminSleepStatsSampleProvider(gbDevice, session)); + + // We may have samples, but not sleep samples - #4048 + // 0 unmeasurable, 1 awake + final boolean anySleepSample = sleepStageSamples.stream() + .anyMatch(s -> s.getStage() != 0 && s.getStage() != 1); + if (anySleepSample) { + persistAbstractSamples(sleepStageSamples, new GarminSleepStageSampleProvider(gbDevice, session)); + } + + processRawSleepSamples(session); + break; + case HRV_STATUS: + persistAbstractSamples(hrvSummarySamples, new GarminHrvSummarySampleProvider(gbDevice, session)); + persistAbstractSamples(hrvValueSamples, new GarminHrvValueSampleProvider(gbDevice, session)); + break; + default: + LOG.warn("Unable to handle fit file of type {}", fileId.getType()); + } + } catch (final Exception e) { + GB.toast(context, "Error saving samples", Toast.LENGTH_LONG, GB.ERROR, e); } for (final Map.Entry e : unknownRecords.entrySet()) { @@ -343,14 +373,13 @@ public class FitImporter { } } - private void persistWorkout(final File file) { + private void persistWorkout(final File file, final DaoSession session) { LOG.debug("Persisting workout for {}", fileId); final BaseActivitySummary summary; // This ensures idempotency when re-processing - try (DBHandler dbHandler = GBApplication.acquireDB()) { - final DaoSession session = dbHandler.getDaoSession(); + try { summary = ActivitySummaryParser.findOrCreateBaseActivitySummary( session, gbDevice, @@ -365,8 +394,7 @@ public class FitImporter { summary.setRawDetailsPath(file.getAbsolutePath()); - try (DBHandler dbHandler = GBApplication.acquireDB()) { - final DaoSession session = dbHandler.getDaoSession(); + try { final Device device = DBHelper.getDevice(gbDevice, session); final User user = DBHelper.getUser(session); @@ -387,6 +415,7 @@ public class FitImporter { respiratoryRateSamples.clear(); restingHrSamples.clear(); events.clear(); + sleepStatsSamples.clear(); sleepStageSamples.clear(); hrvSummarySamples.clear(); hrvValueSamples.clear(); @@ -398,7 +427,7 @@ public class FitImporter { workoutParser.reset(); } - private void persistActivitySamples() { + private void persistActivitySamples(final DaoSession session) { if (activitySamplesPerTimestamp.isEmpty()) { return; } @@ -507,9 +536,7 @@ public class FitImporter { LOG.debug("Will persist {} activity samples", activitySamples.size()); - try (DBHandler handler = GBApplication.acquireDB()) { - final DaoSession session = handler.getDaoSession(); - + try { final Device device = DBHelper.getDevice(gbDevice, session); final User user = DBHelper.getUser(session); @@ -526,69 +553,13 @@ public class FitImporter { } } - private void persistEvents() { - if (events.isEmpty()) { - return; - } - - LOG.debug("Will persist {} event samples", events.size()); - - try (DBHandler handler = GBApplication.acquireDB()) { - final DaoSession session = handler.getDaoSession(); - - final Device device = DBHelper.getDevice(gbDevice, session); - final User user = DBHelper.getUser(session); - - final GarminEventSampleProvider sampleProvider = new GarminEventSampleProvider(gbDevice, session); - - for (final GarminEventSample sample : events) { - sample.setDevice(device); - sample.setUser(user); - } - - sampleProvider.addSamples(events); - } catch (final Exception e) { - GB.toast(context, "Error saving event samples", Toast.LENGTH_LONG, GB.ERROR, e); - } - } - - private void persistSleepStageSamples() { - // We may have samples, but not sleep samples - #4048 - // 0 unmeasurable, 1 awake - final boolean anySleepSample = sleepStageSamples.stream() - .anyMatch(s -> s.getStage() != 0 && s.getStage() != 1); - if (!anySleepSample) { - return; - } - - LOG.debug("Will persist {} sleep stage samples", sleepStageSamples.size()); - - try (DBHandler handler = GBApplication.acquireDB()) { - final DaoSession session = handler.getDaoSession(); - - final Device device = DBHelper.getDevice(gbDevice, session); - final User user = DBHelper.getUser(session); - - final GarminSleepStageSampleProvider sampleProvider = new GarminSleepStageSampleProvider(gbDevice, session); - - for (final GarminSleepStageSample sample : sleepStageSamples) { - sample.setDevice(device); - sample.setUser(user); - } - - sampleProvider.addSamples(sleepStageSamples); - } catch (final Exception e) { - GB.toast(context, "Error saving sleep stage samples", Toast.LENGTH_LONG, GB.ERROR, e); - } - } - /** * As per #4048, devices that do not have a sleep widget send raw sleep samples, which we do not * know how to parse. Therefore, we don't persist the sleep stages they report (they're all awake), * but we fake light sleep for the duration of the raw sleep samples, in order to have some data * at all. */ - private void processRawSleepSamples() { + private void processRawSleepSamples(final DaoSession session) { if (fitSleepDataRawSamples.isEmpty()) { return; } @@ -606,9 +577,7 @@ public class FitImporter { LOG.debug("Got {} raw sleep samples - faking sleep events from {} to {}", fitSleepDataRawSamples.size(), asleepTimeMillis, wakeTimeMillis); // We only need to fake sleep start and end times, the sample provider will take care of the rest - try (DBHandler handler = GBApplication.acquireDB()) { - final DaoSession session = handler.getDaoSession(); - + try { final Device device = DBHelper.getDevice(gbDevice, session); final User user = DBHelper.getUser(session); @@ -637,211 +606,32 @@ public class FitImporter { } } - private void persistHrvSummarySamples() { - if (hrvSummarySamples.isEmpty()) { + private void persistAbstractSamples(final List samples, + final AbstractTimeSampleProvider sampleProvider) { + if (samples.isEmpty()) { return; } - LOG.debug("Will persist {} HRV summary samples", hrvSummarySamples.size()); + LOG.debug( + "Will persist {} {} samples", + samples.size(), + sampleProvider.getClass().getSimpleName().replace("Garmin", "").replace("SampleProvider", "") + ); - try (DBHandler handler = GBApplication.acquireDB()) { - final DaoSession session = handler.getDaoSession(); + try { + final DaoSession session = sampleProvider.getSession(); final Device device = DBHelper.getDevice(gbDevice, session); final User user = DBHelper.getUser(session); - final GarminHrvSummarySampleProvider sampleProvider = new GarminHrvSummarySampleProvider(gbDevice, session); - - for (final GarminHrvSummarySample sample : hrvSummarySamples) { + for (final T sample : samples) { sample.setDevice(device); sample.setUser(user); } - sampleProvider.addSamples(hrvSummarySamples); + sampleProvider.addSamples(samples); } catch (final Exception e) { - GB.toast(context, "Error saving HRV summary samples", Toast.LENGTH_LONG, GB.ERROR, e); - } - } - - private void persistHrvValueSamples() { - if (hrvValueSamples.isEmpty()) { - return; - } - - LOG.debug("Will persist {} HRV value samples", hrvValueSamples.size()); - - try (DBHandler handler = GBApplication.acquireDB()) { - final DaoSession session = handler.getDaoSession(); - - final Device device = DBHelper.getDevice(gbDevice, session); - final User user = DBHelper.getUser(session); - - final GarminHrvValueSampleProvider sampleProvider = new GarminHrvValueSampleProvider(gbDevice, session); - - for (final GarminHrvValueSample sample : hrvValueSamples) { - sample.setDevice(device); - sample.setUser(user); - } - - sampleProvider.addSamples(hrvValueSamples); - } catch (final Exception e) { - GB.toast(context, "Error saving HRV value samples", Toast.LENGTH_LONG, GB.ERROR, e); - } - } - - private void persistSpo2Samples() { - if (spo2samples.isEmpty()) { - return; - } - - LOG.debug("Will persist {} spo2 samples", spo2samples.size()); - - try (DBHandler handler = GBApplication.acquireDB()) { - final DaoSession session = handler.getDaoSession(); - - final Device device = DBHelper.getDevice(gbDevice, session); - final User user = DBHelper.getUser(session); - - final GarminSpo2SampleProvider sampleProvider = new GarminSpo2SampleProvider(gbDevice, session); - - for (final GarminSpo2Sample sample : spo2samples) { - sample.setDevice(device); - sample.setUser(user); - } - - sampleProvider.addSamples(spo2samples); - } catch (final Exception e) { - GB.toast(context, "Error saving spo2 samples", Toast.LENGTH_LONG, GB.ERROR, e); - } - } - - private void persistRespiratoryRateSamples() { - if (respiratoryRateSamples.isEmpty()) { - return; - } - - LOG.debug("Will persist {} respiratory rate samples", stressSamples.size()); - - try (DBHandler handler = GBApplication.acquireDB()) { - final DaoSession session = handler.getDaoSession(); - - final Device device = DBHelper.getDevice(gbDevice, session); - final User user = DBHelper.getUser(session); - - final GarminRespiratoryRateSampleProvider sampleProvider = new GarminRespiratoryRateSampleProvider(gbDevice, session); - - for (final GarminRespiratoryRateSample sample : respiratoryRateSamples) { - sample.setDevice(device); - sample.setUser(user); - } - - sampleProvider.addSamples(respiratoryRateSamples); - } catch (final Exception e) { - GB.toast(context, "Error saving respiratory rate samples", Toast.LENGTH_LONG, GB.ERROR, e); - } - } - - private void persistRestingHrSamples() { - if (restingHrSamples.isEmpty()) { - return; - } - - LOG.debug("Will persist {} resting heart rate samples", restingHrSamples.size()); - - try (DBHandler handler = GBApplication.acquireDB()) { - final DaoSession session = handler.getDaoSession(); - - final Device device = DBHelper.getDevice(gbDevice, session); - final User user = DBHelper.getUser(session); - - final GarminHeartRateRestingSampleProvider sampleProvider = new GarminHeartRateRestingSampleProvider(gbDevice, session); - - for (final GarminHeartRateRestingSample sample : restingHrSamples) { - sample.setDevice(device); - sample.setUser(user); - } - - sampleProvider.addSamples(restingHrSamples); - } catch (final Exception e) { - GB.toast(context, "Error saving resting heart rate samples", Toast.LENGTH_LONG, GB.ERROR, e); - } - } - - private void persistStressSamples() { - if (stressSamples.isEmpty()) { - return; - } - - LOG.debug("Will persist {} stress samples", stressSamples.size()); - - try (DBHandler handler = GBApplication.acquireDB()) { - final DaoSession session = handler.getDaoSession(); - - final Device device = DBHelper.getDevice(gbDevice, session); - final User user = DBHelper.getUser(session); - - final GarminStressSampleProvider sampleProvider = new GarminStressSampleProvider(gbDevice, session); - - for (final GarminStressSample sample : stressSamples) { - sample.setDevice(device); - sample.setUser(user); - } - - sampleProvider.addSamples(stressSamples); - } catch (final Exception e) { - GB.toast(context, "Error saving stress samples", Toast.LENGTH_LONG, GB.ERROR, e); - } - } - - private void persistBodyEnergySamples() { - if (bodyEnergySamples.isEmpty()) { - return; - } - - LOG.debug("Will persist {} body energy samples", bodyEnergySamples.size()); - - try (DBHandler handler = GBApplication.acquireDB()) { - final DaoSession session = handler.getDaoSession(); - - final Device device = DBHelper.getDevice(gbDevice, session); - final User user = DBHelper.getUser(session); - - final GarminBodyEnergySampleProvider sampleProvider = new GarminBodyEnergySampleProvider(gbDevice, session); - - for (final GarminBodyEnergySample sample : bodyEnergySamples) { - sample.setDevice(device); - sample.setUser(user); - } - - sampleProvider.addSamples(bodyEnergySamples); - } catch (final Exception e) { - GB.toast(context, "Error saving body energy samples", Toast.LENGTH_LONG, GB.ERROR, e); - } - } - - private void persistRestingMetabolicRateSamples() { - if (restingMetabolicRateSamples.isEmpty()) { - return; - } - - LOG.debug("Will persist {} resting metabolic rate samples", restingMetabolicRateSamples.size()); - - try (DBHandler handler = GBApplication.acquireDB()) { - final DaoSession session = handler.getDaoSession(); - - final Device device = DBHelper.getDevice(gbDevice, session); - final User user = DBHelper.getUser(session); - - final GarminRestingMetabolicRateSampleProvider sampleProvider = new GarminRestingMetabolicRateSampleProvider(gbDevice, session); - - for (final GarminRestingMetabolicRateSample sample : restingMetabolicRateSamples) { - sample.setDevice(device); - sample.setUser(user); - } - - sampleProvider.addSamples(restingMetabolicRateSamples); - } catch (final Exception e) { - GB.toast(context, "Error saving body energy samples", Toast.LENGTH_LONG, GB.ERROR, e); + GB.toast(context, "Error saving samples", Toast.LENGTH_LONG, GB.ERROR, e); } } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/GlobalFITMessage.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/GlobalFITMessage.java index 5ee597c84..becc0b71f 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/GlobalFITMessage.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/GlobalFITMessage.java @@ -363,6 +363,23 @@ public class GlobalFITMessage { )); public static GlobalFITMessage SLEEP_STATS = new GlobalFITMessage(346, "SLEEP_STATS", Arrays.asList( + new FieldDefinitionPrimitive(0, BaseType.UINT8, "combined_awake_score"), + new FieldDefinitionPrimitive(1, BaseType.UINT8, "awake_time_score"), + new FieldDefinitionPrimitive(2, BaseType.UINT8, "awakenings_count_score"), + new FieldDefinitionPrimitive(3, BaseType.UINT8, "deep_sleep_score"), + new FieldDefinitionPrimitive(4, BaseType.UINT8, "sleep_duration_score"), + new FieldDefinitionPrimitive(5, BaseType.UINT8, "light_sleep_score"), + new FieldDefinitionPrimitive(6, BaseType.UINT8, "overall_sleep_score"), + new FieldDefinitionPrimitive(7, BaseType.UINT8, "sleep_quality_score"), + new FieldDefinitionPrimitive(8, BaseType.UINT8, "sleep_recovery_score"), + new FieldDefinitionPrimitive(9, BaseType.UINT8, "rem_sleep_score"), + new FieldDefinitionPrimitive(10, BaseType.UINT8, "sleep_restlessness_score"), + new FieldDefinitionPrimitive(11, BaseType.UINT8, "awakenings_count"), + new FieldDefinitionPrimitive(12, BaseType.ENUM, "unk_12"), + new FieldDefinitionPrimitive(13, BaseType.ENUM, "unk_13"), + new FieldDefinitionPrimitive(14, BaseType.UINT8, "interruptions_score"), + new FieldDefinitionPrimitive(15, BaseType.UINT16, "average_stress_during_sleep", 100, 0), + new FieldDefinitionPrimitive(16, BaseType.ENUM, "unk_16") )); public static GlobalFITMessage HRV_SUMMARY = new GlobalFITMessage(370, "HRV_SUMMARY", Arrays.asList( diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/messages/FitSleepStats.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/messages/FitSleepStats.java index 5d70a4aab..75f1b0b8a 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/messages/FitSleepStats.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/messages/FitSleepStats.java @@ -19,4 +19,89 @@ public class FitSleepStats extends RecordData { throw new IllegalArgumentException("FitSleepStats expects global messages of " + 346 + ", got " + globalNumber); } } + + @Nullable + public Integer getCombinedAwakeScore() { + return (Integer) getFieldByNumber(0); + } + + @Nullable + public Integer getAwakeTimeScore() { + return (Integer) getFieldByNumber(1); + } + + @Nullable + public Integer getAwakeningsCountScore() { + return (Integer) getFieldByNumber(2); + } + + @Nullable + public Integer getDeepSleepScore() { + return (Integer) getFieldByNumber(3); + } + + @Nullable + public Integer getSleepDurationScore() { + return (Integer) getFieldByNumber(4); + } + + @Nullable + public Integer getLightSleepScore() { + return (Integer) getFieldByNumber(5); + } + + @Nullable + public Integer getOverallSleepScore() { + return (Integer) getFieldByNumber(6); + } + + @Nullable + public Integer getSleepQualityScore() { + return (Integer) getFieldByNumber(7); + } + + @Nullable + public Integer getSleepRecoveryScore() { + return (Integer) getFieldByNumber(8); + } + + @Nullable + public Integer getRemSleepScore() { + return (Integer) getFieldByNumber(9); + } + + @Nullable + public Integer getSleepRestlessnessScore() { + return (Integer) getFieldByNumber(10); + } + + @Nullable + public Integer getAwakeningsCount() { + return (Integer) getFieldByNumber(11); + } + + @Nullable + public Integer getUnk12() { + return (Integer) getFieldByNumber(12); + } + + @Nullable + public Integer getUnk13() { + return (Integer) getFieldByNumber(13); + } + + @Nullable + public Integer getInterruptionsScore() { + return (Integer) getFieldByNumber(14); + } + + @Nullable + public Float getAverageStressDuringSleep() { + return (Float) getFieldByNumber(15); + } + + @Nullable + public Integer getUnk16() { + return (Integer) getFieldByNumber(16); + } }