Garmin: Persist sleep score

This commit is contained in:
José Rebelo 2024-11-24 19:44:18 +00:00
parent 5531ddc49a
commit c52fd53ebd
12 changed files with 338 additions and 287 deletions

View File

@ -54,7 +54,7 @@ public class GBDaoGenerator {
public static void main(String[] args) throws Exception { 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 userAttributes = addUserAttributes(schema);
Entity user = addUserInfo(schema, userAttributes); Entity user = addUserInfo(schema, userAttributes);
@ -128,6 +128,7 @@ public class GBDaoGenerator {
addGarminRespiratoryRateSample(schema, user, device); addGarminRespiratoryRateSample(schema, user, device);
addGarminHeartRateRestingSample(schema, user, device); addGarminHeartRateRestingSample(schema, user, device);
addGarminRestingMetabolicRateSample(schema, user, device); addGarminRestingMetabolicRateSample(schema, user, device);
addGarminSleepStatsSample(schema, user, device);
addPendingFile(schema, user, device); addPendingFile(schema, user, device);
addWena3EnergySample(schema, user, device); addWena3EnergySample(schema, user, device);
addWena3BehaviorSample(schema, user, device); addWena3BehaviorSample(schema, user, device);
@ -879,6 +880,14 @@ public class GBDaoGenerator {
return sample; 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) { private static Entity addPendingFile(Schema schema, Entity user, Entity device) {
Entity pendingFile = addEntity(schema, "PendingFile"); Entity pendingFile = addEntity(schema, "PendingFile");
pendingFile.setJavaDoc( pendingFile.setJavaDoc(

View File

@ -78,6 +78,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.HrvValueSample;
import nodomain.freeyourgadget.gadgetbridge.model.PaiSample; import nodomain.freeyourgadget.gadgetbridge.model.PaiSample;
import nodomain.freeyourgadget.gadgetbridge.model.RespiratoryRateSample; import nodomain.freeyourgadget.gadgetbridge.model.RespiratoryRateSample;
import nodomain.freeyourgadget.gadgetbridge.model.RestingMetabolicRateSample; import nodomain.freeyourgadget.gadgetbridge.model.RestingMetabolicRateSample;
import nodomain.freeyourgadget.gadgetbridge.model.SleepScoreSample;
import nodomain.freeyourgadget.gadgetbridge.model.Spo2Sample; import nodomain.freeyourgadget.gadgetbridge.model.Spo2Sample;
import nodomain.freeyourgadget.gadgetbridge.model.StressSample; import nodomain.freeyourgadget.gadgetbridge.model.StressSample;
import nodomain.freeyourgadget.gadgetbridge.model.TemperatureSample; import nodomain.freeyourgadget.gadgetbridge.model.TemperatureSample;
@ -286,6 +287,11 @@ public abstract class AbstractDeviceCoordinator implements DeviceCoordinator {
return new DefaultRestingMetabolicRateProvider(device, session); return new DefaultRestingMetabolicRateProvider(device, session);
} }
@Override
public TimeSampleProvider<? extends SleepScoreSample> getSleepScoreProvider(final GBDevice device, final DaoSession session) {
return null;
}
@Override @Override
@Nullable @Nullable
public ActivitySummaryParser getActivitySummaryParser(final GBDevice device, final Context context) { public ActivitySummaryParser getActivitySummaryParser(final GBDevice device, final Context context) {
@ -684,6 +690,11 @@ public abstract class AbstractDeviceCoordinator implements DeviceCoordinator {
return false; return false;
} }
@Override
public boolean supportsSleepScore() {
return false;
}
@Override @Override
public boolean supportsWeather() { public boolean supportsWeather() {
return false; return false;

View File

@ -25,6 +25,8 @@ import java.util.List;
import de.greenrobot.dao.AbstractDao; import de.greenrobot.dao.AbstractDao;
import de.greenrobot.dao.Property; import de.greenrobot.dao.Property;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; 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.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser; import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser;
import nodomain.freeyourgadget.gadgetbridge.model.RestingMetabolicRateSample; 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 @Override
public long getTimestamp() { public long getTimestamp() {
return timestamp; return timestamp;

View File

@ -58,6 +58,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.HrvValueSample;
import nodomain.freeyourgadget.gadgetbridge.model.PaiSample; import nodomain.freeyourgadget.gadgetbridge.model.PaiSample;
import nodomain.freeyourgadget.gadgetbridge.model.RespiratoryRateSample; import nodomain.freeyourgadget.gadgetbridge.model.RespiratoryRateSample;
import nodomain.freeyourgadget.gadgetbridge.model.RestingMetabolicRateSample; import nodomain.freeyourgadget.gadgetbridge.model.RestingMetabolicRateSample;
import nodomain.freeyourgadget.gadgetbridge.model.SleepScoreSample;
import nodomain.freeyourgadget.gadgetbridge.model.Spo2Sample; import nodomain.freeyourgadget.gadgetbridge.model.Spo2Sample;
import nodomain.freeyourgadget.gadgetbridge.model.StressSample; import nodomain.freeyourgadget.gadgetbridge.model.StressSample;
import nodomain.freeyourgadget.gadgetbridge.model.TemperatureSample; import nodomain.freeyourgadget.gadgetbridge.model.TemperatureSample;
@ -376,6 +377,8 @@ public interface DeviceCoordinator {
TimeSampleProvider<? extends RestingMetabolicRateSample> getRestingMetabolicRateProvider(GBDevice device, DaoSession session); TimeSampleProvider<? extends RestingMetabolicRateSample> getRestingMetabolicRateProvider(GBDevice device, DaoSession session);
TimeSampleProvider<? extends SleepScoreSample> getSleepScoreProvider(GBDevice device, DaoSession session);
/** /**
* Returns the {@link ActivitySummaryParser} for the device being supported. * Returns the {@link ActivitySummaryParser} for the device being supported.
* *
@ -574,6 +577,11 @@ public interface DeviceCoordinator {
*/ */
boolean supportsAwakeSleep(); 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 * Indicates whether the device supports current weather and/or weather
* forecast display. * forecast display.

View File

@ -46,6 +46,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.HrvSummarySample;
import nodomain.freeyourgadget.gadgetbridge.model.HrvValueSample; import nodomain.freeyourgadget.gadgetbridge.model.HrvValueSample;
import nodomain.freeyourgadget.gadgetbridge.model.RespiratoryRateSample; import nodomain.freeyourgadget.gadgetbridge.model.RespiratoryRateSample;
import nodomain.freeyourgadget.gadgetbridge.model.RestingMetabolicRateSample; import nodomain.freeyourgadget.gadgetbridge.model.RestingMetabolicRateSample;
import nodomain.freeyourgadget.gadgetbridge.model.SleepScoreSample;
import nodomain.freeyourgadget.gadgetbridge.model.Spo2Sample; import nodomain.freeyourgadget.gadgetbridge.model.Spo2Sample;
import nodomain.freeyourgadget.gadgetbridge.model.StressSample; import nodomain.freeyourgadget.gadgetbridge.model.StressSample;
import nodomain.freeyourgadget.gadgetbridge.model.Vo2MaxSample; import nodomain.freeyourgadget.gadgetbridge.model.Vo2MaxSample;
@ -159,6 +160,11 @@ public abstract class GarminCoordinator extends AbstractBLEDeviceCoordinator {
return new GarminRestingMetabolicRateSampleProvider(device, session); return new GarminRestingMetabolicRateSampleProvider(device, session);
} }
@Override
public TimeSampleProvider<? extends SleepScoreSample> getSleepScoreProvider(final GBDevice device, final DaoSession session) {
return new GarminSleepStatsSampleProvider(device, session);
}
@Override @Override
public GarminHeartRateRestingSampleProvider getHeartRateRestingSampleProvider(final GBDevice device, final DaoSession session) { public GarminHeartRateRestingSampleProvider getHeartRateRestingSampleProvider(final GBDevice device, final DaoSession session) {
return new GarminHeartRateRestingSampleProvider(device, session); return new GarminHeartRateRestingSampleProvider(device, session);
@ -294,6 +300,11 @@ public abstract class GarminCoordinator extends AbstractBLEDeviceCoordinator {
return true; return true;
} }
@Override
public boolean supportsSleepScore() {
return true;
}
@Override @Override
public boolean supportsRespiratoryRate() { public boolean supportsRespiratoryRate() {
return true; return true;

View File

@ -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 <https://www.gnu.org/licenses/>. */
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<GarminSleepStatsSample> {
public GarminSleepStatsSampleProvider(final GBDevice device, final DaoSession session) {
super(device, session);
}
@NonNull
@Override
public AbstractDao<GarminSleepStatsSample, ?> 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();
}
}

View File

@ -35,6 +35,7 @@ import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.Device; import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiActivitySample; import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiActivitySample;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiActivitySampleDao; import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiActivitySampleDao;
import nodomain.freeyourgadget.gadgetbridge.entities.User;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
public class HuaweiSpo2SampleProvider extends AbstractTimeSampleProvider<HuaweiSpo2SampleProvider.HuaweiSpo2Sample> { public class HuaweiSpo2SampleProvider extends AbstractTimeSampleProvider<HuaweiSpo2SampleProvider.HuaweiSpo2Sample> {
@ -206,6 +207,16 @@ public class HuaweiSpo2SampleProvider extends AbstractTimeSampleProvider<HuaweiS
this.deviceId = deviceId; this.deviceId = deviceId;
} }
@Override
public void setDevice(final Device device) {
}
@Override
public void setUser(final User user) {
}
@Override @Override
public int getSpo2() { public int getSpo2() {
return spo2; return spo2;

View File

@ -33,6 +33,10 @@ public abstract class AbstractTimeSample implements TimeSample {
public abstract void setDeviceId(long deviceId); public abstract void setDeviceId(long deviceId);
public abstract void setDevice(Device device);
public abstract void setUser(User user);
@NonNull @NonNull
@Override @Override
public String toString() { public String toString() {

View File

@ -0,0 +1,37 @@
/* 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 <https://www.gnu.org/licenses/>. */
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() +
"}";
}
}

View File

@ -19,6 +19,7 @@ import java.util.TreeMap;
import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper; 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.GarminActivitySampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminBodyEnergySampleProvider; import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminBodyEnergySampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminEventSampleProvider; 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.GarminHrvValueSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminRespiratoryRateSampleProvider; import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminRespiratoryRateSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminRestingMetabolicRateSampleProvider; 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.GarminSleepStageSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminSpo2SampleProvider; import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminSpo2SampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminStressSampleProvider; import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminStressSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminWorkoutParser; import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminWorkoutParser;
import nodomain.freeyourgadget.gadgetbridge.entities.AbstractTimeSample;
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary; import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.Device; 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.GarminHrvValueSample;
import nodomain.freeyourgadget.gadgetbridge.entities.GarminRespiratoryRateSample; import nodomain.freeyourgadget.gadgetbridge.entities.GarminRespiratoryRateSample;
import nodomain.freeyourgadget.gadgetbridge.entities.GarminSleepStageSample; import nodomain.freeyourgadget.gadgetbridge.entities.GarminSleepStageSample;
import nodomain.freeyourgadget.gadgetbridge.entities.GarminSleepStatsSample;
import nodomain.freeyourgadget.gadgetbridge.entities.GarminSpo2Sample; import nodomain.freeyourgadget.gadgetbridge.entities.GarminSpo2Sample;
import nodomain.freeyourgadget.gadgetbridge.entities.GarminStressSample; import nodomain.freeyourgadget.gadgetbridge.entities.GarminStressSample;
import nodomain.freeyourgadget.gadgetbridge.entities.User; 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.ActivityKind;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample; import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser; 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.FileType;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionHrvStatus; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionHrvStatus;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionSleepStage; 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.FitSleepDataInfo;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitSleepDataRaw; 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.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.FitSpo2;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitSport; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitSport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitStressLevel; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitStressLevel;
@ -87,6 +91,7 @@ public class FitImporter {
private final List<GarminRespiratoryRateSample> respiratoryRateSamples = new ArrayList<>(); private final List<GarminRespiratoryRateSample> respiratoryRateSamples = new ArrayList<>();
private final List<GarminHeartRateRestingSample> restingHrSamples = new ArrayList<>(); private final List<GarminHeartRateRestingSample> restingHrSamples = new ArrayList<>();
private final List<GarminEventSample> events = new ArrayList<>(); private final List<GarminEventSample> events = new ArrayList<>();
private final List<GarminSleepStatsSample> sleepStatsSamples = new ArrayList<>();
private final List<GarminSleepStageSample> sleepStageSamples = new ArrayList<>(); private final List<GarminSleepStageSample> sleepStageSamples = new ArrayList<>();
private final List<GarminHrvSummarySample> hrvSummarySamples = new ArrayList<>(); private final List<GarminHrvSummarySample> hrvSummarySamples = new ArrayList<>();
private final List<GarminHrvValueSample> hrvValueSamples = new ArrayList<>(); private final List<GarminHrvValueSample> hrvValueSamples = new ArrayList<>();
@ -162,6 +167,16 @@ public class FitImporter {
final FitSleepDataRaw fitSleepDataRaw = (FitSleepDataRaw) record; final FitSleepDataRaw fitSleepDataRaw = (FitSleepDataRaw) record;
//LOG.debug("Sleep Data Raw: {}", fitSleepDataRaw); //LOG.debug("Sleep Data Raw: {}", fitSleepDataRaw);
fitSleepDataRawSamples.add(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) { } else if (record instanceof FitSleepStage) {
final FieldDefinitionSleepStage.SleepStage stage = ((FitSleepStage) record).getSleepStage(); final FieldDefinitionSleepStage.SleepStage stage = ((FitSleepStage) record).getSleepStage();
if (stage == null) { if (stage == null) {
@ -312,30 +327,45 @@ public class FitImporter {
return; return;
} }
switch (fileId.getType()) { try (DBHandler handler = GBApplication.acquireDB()) {
case ACTIVITY: final DaoSession session = handler.getDaoSession();
persistWorkout(file);
break; switch (fileId.getType()) {
case MONITOR: case ACTIVITY:
persistActivitySamples(); persistWorkout(file, session);
persistSpo2Samples(); break;
persistRespiratoryRateSamples(); case MONITOR:
persistRestingHrSamples(); persistActivitySamples(session);
persistStressSamples(); persistAbstractSamples(spo2samples, new GarminSpo2SampleProvider(gbDevice, session));
persistBodyEnergySamples(); persistAbstractSamples(respiratoryRateSamples, new GarminRespiratoryRateSampleProvider(gbDevice, session));
persistRestingMetabolicRateSamples(); persistAbstractSamples(restingHrSamples, new GarminHeartRateRestingSampleProvider(gbDevice, session));
break; persistAbstractSamples(stressSamples, new GarminStressSampleProvider(gbDevice, session));
case SLEEP: persistAbstractSamples(bodyEnergySamples, new GarminBodyEnergySampleProvider(gbDevice, session));
persistEvents(); persistAbstractSamples(restingMetabolicRateSamples, new GarminRestingMetabolicRateSampleProvider(gbDevice, session));
persistSleepStageSamples(); break;
processRawSleepSamples(); case SLEEP:
break; persistAbstractSamples(events, new GarminEventSampleProvider(gbDevice, session));
case HRV_STATUS: persistAbstractSamples(sleepStatsSamples, new GarminSleepStatsSampleProvider(gbDevice, session));
persistHrvSummarySamples();
persistHrvValueSamples(); // We may have samples, but not sleep samples - #4048
break; // 0 unmeasurable, 1 awake
default: final boolean anySleepSample = sleepStageSamples.stream()
LOG.warn("Unable to handle fit file of type {}", fileId.getType()); .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<Integer, Integer> e : unknownRecords.entrySet()) { for (final Map.Entry<Integer, Integer> 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); LOG.debug("Persisting workout for {}", fileId);
final BaseActivitySummary summary; final BaseActivitySummary summary;
// This ensures idempotency when re-processing // This ensures idempotency when re-processing
try (DBHandler dbHandler = GBApplication.acquireDB()) { try {
final DaoSession session = dbHandler.getDaoSession();
summary = ActivitySummaryParser.findOrCreateBaseActivitySummary( summary = ActivitySummaryParser.findOrCreateBaseActivitySummary(
session, session,
gbDevice, gbDevice,
@ -365,8 +394,7 @@ public class FitImporter {
summary.setRawDetailsPath(file.getAbsolutePath()); summary.setRawDetailsPath(file.getAbsolutePath());
try (DBHandler dbHandler = GBApplication.acquireDB()) { try {
final DaoSession session = dbHandler.getDaoSession();
final Device device = DBHelper.getDevice(gbDevice, session); final Device device = DBHelper.getDevice(gbDevice, session);
final User user = DBHelper.getUser(session); final User user = DBHelper.getUser(session);
@ -387,6 +415,7 @@ public class FitImporter {
respiratoryRateSamples.clear(); respiratoryRateSamples.clear();
restingHrSamples.clear(); restingHrSamples.clear();
events.clear(); events.clear();
sleepStatsSamples.clear();
sleepStageSamples.clear(); sleepStageSamples.clear();
hrvSummarySamples.clear(); hrvSummarySamples.clear();
hrvValueSamples.clear(); hrvValueSamples.clear();
@ -398,7 +427,7 @@ public class FitImporter {
workoutParser.reset(); workoutParser.reset();
} }
private void persistActivitySamples() { private void persistActivitySamples(final DaoSession session) {
if (activitySamplesPerTimestamp.isEmpty()) { if (activitySamplesPerTimestamp.isEmpty()) {
return; return;
} }
@ -507,9 +536,7 @@ public class FitImporter {
LOG.debug("Will persist {} activity samples", activitySamples.size()); LOG.debug("Will persist {} activity samples", activitySamples.size());
try (DBHandler handler = GBApplication.acquireDB()) { try {
final DaoSession session = handler.getDaoSession();
final Device device = DBHelper.getDevice(gbDevice, session); final Device device = DBHelper.getDevice(gbDevice, session);
final User user = DBHelper.getUser(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 * 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), * 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 * but we fake light sleep for the duration of the raw sleep samples, in order to have some data
* at all. * at all.
*/ */
private void processRawSleepSamples() { private void processRawSleepSamples(final DaoSession session) {
if (fitSleepDataRawSamples.isEmpty()) { if (fitSleepDataRawSamples.isEmpty()) {
return; return;
} }
@ -606,9 +577,7 @@ public class FitImporter {
LOG.debug("Got {} raw sleep samples - faking sleep events from {} to {}", fitSleepDataRawSamples.size(), asleepTimeMillis, wakeTimeMillis); 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 // We only need to fake sleep start and end times, the sample provider will take care of the rest
try (DBHandler handler = GBApplication.acquireDB()) { try {
final DaoSession session = handler.getDaoSession();
final Device device = DBHelper.getDevice(gbDevice, session); final Device device = DBHelper.getDevice(gbDevice, session);
final User user = DBHelper.getUser(session); final User user = DBHelper.getUser(session);
@ -637,211 +606,32 @@ public class FitImporter {
} }
} }
private void persistHrvSummarySamples() { private <T extends AbstractTimeSample> void persistAbstractSamples(final List<T> samples,
if (hrvSummarySamples.isEmpty()) { final AbstractTimeSampleProvider<T> sampleProvider) {
if (samples.isEmpty()) {
return; 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()) { try {
final DaoSession session = handler.getDaoSession(); final DaoSession session = sampleProvider.getSession();
final Device device = DBHelper.getDevice(gbDevice, session); final Device device = DBHelper.getDevice(gbDevice, session);
final User user = DBHelper.getUser(session); final User user = DBHelper.getUser(session);
final GarminHrvSummarySampleProvider sampleProvider = new GarminHrvSummarySampleProvider(gbDevice, session); for (final T sample : samples) {
for (final GarminHrvSummarySample sample : hrvSummarySamples) {
sample.setDevice(device); sample.setDevice(device);
sample.setUser(user); sample.setUser(user);
} }
sampleProvider.addSamples(hrvSummarySamples); sampleProvider.addSamples(samples);
} catch (final Exception e) { } catch (final Exception e) {
GB.toast(context, "Error saving HRV summary samples", Toast.LENGTH_LONG, GB.ERROR, e); GB.toast(context, "Error saving 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);
} }
} }
} }

View File

@ -363,6 +363,23 @@ public class GlobalFITMessage {
)); ));
public static GlobalFITMessage SLEEP_STATS = new GlobalFITMessage(346, "SLEEP_STATS", Arrays.asList( 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( public static GlobalFITMessage HRV_SUMMARY = new GlobalFITMessage(370, "HRV_SUMMARY", Arrays.asList(

View File

@ -19,4 +19,89 @@ public class FitSleepStats extends RecordData {
throw new IllegalArgumentException("FitSleepStats expects global messages of " + 346 + ", got " + globalNumber); 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);
}
} }