diff --git a/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java b/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java index eb7210d08..2e8b101d6 100644 --- a/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java +++ b/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java @@ -45,7 +45,7 @@ public class GBDaoGenerator { public static void main(String[] args) throws Exception { - final Schema schema = new Schema(74, MAIN_PACKAGE + ".entities"); + final Schema schema = new Schema(75, MAIN_PACKAGE + ".entities"); Entity userAttributes = addUserAttributes(schema); Entity user = addUserInfo(schema, userAttributes); @@ -113,6 +113,8 @@ public class GBDaoGenerator { addGarminSpo2Sample(schema, user, device); addGarminSleepStageSample(schema, user, device); addGarminEventSample(schema, user, device); + addGarminHrvSummarySample(schema, user, device); + addGarminHrvValueSample(schema, user, device); addWena3EnergySample(schema, user, device); addWena3BehaviorSample(schema, user, device); addWena3CaloriesSample(schema, user, device); @@ -725,6 +727,26 @@ public class GBDaoGenerator { return sleepStageSample; } + private static Entity addGarminHrvSummarySample(Schema schema, Entity user, Entity device) { + Entity hrvSummarySample = addEntity(schema, "GarminHrvSummarySample"); + addCommonTimeSampleProperties("AbstractHrvSummarySample", hrvSummarySample, user, device); + hrvSummarySample.addIntProperty("weeklyAverage").codeBeforeGetter(OVERRIDE); + hrvSummarySample.addIntProperty("lastNightAverage").codeBeforeGetter(OVERRIDE); + hrvSummarySample.addIntProperty("lastNight5MinHigh").codeBeforeGetter(OVERRIDE); + hrvSummarySample.addIntProperty("baselineLowUpper").codeBeforeGetter(OVERRIDE); + hrvSummarySample.addIntProperty("baselineBalancedLower").codeBeforeGetter(OVERRIDE); + hrvSummarySample.addIntProperty("baselineBalancedUpper").codeBeforeGetter(OVERRIDE); + hrvSummarySample.addIntProperty("statusNum").codeBeforeGetter(OVERRIDE); + return hrvSummarySample; + } + + private static Entity addGarminHrvValueSample(Schema schema, Entity user, Entity device) { + Entity hrvValueSample = addEntity(schema, "GarminHrvValueSample"); + addCommonTimeSampleProperties("AbstractHrvValueSample", hrvValueSample, user, device); + hrvValueSample.addIntProperty("value").notNull().codeBeforeGetter(OVERRIDE); + return hrvValueSample; + } + private static Entity addWatchXPlusHealthActivitySample(Schema schema, Entity user, Entity device) { Entity activitySample = addEntity(schema, "WatchXPlusActivitySample"); activitySample.implementsSerializable(); 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 2ed27c5c6..a229354b7 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractDeviceCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractDeviceCoordinator.java @@ -73,6 +73,8 @@ import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser; import nodomain.freeyourgadget.gadgetbridge.model.BatteryConfig; import nodomain.freeyourgadget.gadgetbridge.model.DeviceType; import nodomain.freeyourgadget.gadgetbridge.model.HeartRateSample; +import nodomain.freeyourgadget.gadgetbridge.model.HrvSummarySample; +import nodomain.freeyourgadget.gadgetbridge.model.HrvValueSample; import nodomain.freeyourgadget.gadgetbridge.model.PaiSample; import nodomain.freeyourgadget.gadgetbridge.model.SleepRespiratoryRateSample; import nodomain.freeyourgadget.gadgetbridge.model.Spo2Sample; @@ -208,6 +210,16 @@ public abstract class AbstractDeviceCoordinator implements DeviceCoordinator { return null; } + @Override + public TimeSampleProvider getHrvSummarySampleProvider(GBDevice device, DaoSession session) { + return null; + } + + @Override + public TimeSampleProvider getHrvValueSampleProvider(GBDevice device, DaoSession session) { + return null; + } + @Override public int[] getStressRanges() { // 0-39 = relaxed @@ -442,6 +454,11 @@ public abstract class AbstractDeviceCoordinator implements DeviceCoordinator { return false; } + @Override + public boolean supportsHrvMeasurement() { + return false; + } + @Override public boolean supportsActivityTabs() { return supportsActivityTracking(); 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 c58b6e0bb..fde5a3f18 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceCoordinator.java @@ -52,6 +52,8 @@ import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser; import nodomain.freeyourgadget.gadgetbridge.model.BatteryConfig; import nodomain.freeyourgadget.gadgetbridge.model.DeviceType; import nodomain.freeyourgadget.gadgetbridge.model.HeartRateSample; +import nodomain.freeyourgadget.gadgetbridge.model.HrvSummarySample; +import nodomain.freeyourgadget.gadgetbridge.model.HrvValueSample; import nodomain.freeyourgadget.gadgetbridge.model.PaiSample; import nodomain.freeyourgadget.gadgetbridge.model.SleepRespiratoryRateSample; import nodomain.freeyourgadget.gadgetbridge.model.Spo2Sample; @@ -214,6 +216,8 @@ public interface DeviceCoordinator { */ boolean supportsStressMeasurement(); + boolean supportsHrvMeasurement(); + boolean supportsSleepMeasurement(); boolean supportsStepCounter(); boolean supportsSpeedzones(); @@ -283,6 +287,16 @@ public interface DeviceCoordinator { */ TimeSampleProvider getStressSampleProvider(GBDevice device, DaoSession session); + /** + * Returns the sample provider for HRV summary, for the device being supported. + */ + TimeSampleProvider getHrvSummarySampleProvider(GBDevice device, DaoSession session); + + /** + * Returns the sample provider for HRV values, for the device being supported. + */ + TimeSampleProvider getHrvValueSampleProvider(GBDevice device, DaoSession session); + /** * Returns the stress ranges (relaxed, mild, moderate, high), so that stress can be categorized. */ 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 ffe959c2c..546ca95f6 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 @@ -24,6 +24,8 @@ import nodomain.freeyourgadget.gadgetbridge.entities.GarminSpo2SampleDao; import nodomain.freeyourgadget.gadgetbridge.entities.GarminStressSampleDao; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample; +import nodomain.freeyourgadget.gadgetbridge.model.HrvSummarySample; +import nodomain.freeyourgadget.gadgetbridge.model.HrvValueSample; import nodomain.freeyourgadget.gadgetbridge.model.Spo2Sample; import nodomain.freeyourgadget.gadgetbridge.model.StressSample; import nodomain.freeyourgadget.gadgetbridge.service.DeviceSupport; @@ -96,6 +98,16 @@ public abstract class GarminCoordinator extends AbstractBLEDeviceCoordinator { return new GarminStressSampleProvider(device, session); } + @Override + public TimeSampleProvider getHrvSummarySampleProvider(final GBDevice device, final DaoSession session) { + return new GarminHrvSummarySampleProvider(device, session); + } + + @Override + public TimeSampleProvider getHrvValueSampleProvider(final GBDevice device, final DaoSession session) { + return new GarminHrvValueSampleProvider(device, session); + } + @Override public TimeSampleProvider getSpo2SampleProvider(final GBDevice device, final DaoSession session) { return new GarminSpo2SampleProvider(device, session); @@ -162,6 +174,11 @@ public abstract class GarminCoordinator extends AbstractBLEDeviceCoordinator { return true; } + @Override + public boolean supportsHrvMeasurement() { + return true; + } + @Override public int[] getStressRanges() { // 1-25 = relaxed diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/GarminHrvSummarySampleProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/GarminHrvSummarySampleProvider.java new file mode 100644 index 000000000..90537472c --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/GarminHrvSummarySampleProvider.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.GarminHrvSummarySample; +import nodomain.freeyourgadget.gadgetbridge.entities.GarminHrvSummarySampleDao; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; + +public class GarminHrvSummarySampleProvider extends AbstractTimeSampleProvider { + public GarminHrvSummarySampleProvider(final GBDevice device, final DaoSession session) { + super(device, session); + } + + @NonNull + @Override + public AbstractDao getSampleDao() { + return getSession().getGarminHrvSummarySampleDao(); + } + + @NonNull + @Override + protected Property getTimestampSampleProperty() { + return GarminHrvSummarySampleDao.Properties.Timestamp; + } + + @NonNull + @Override + protected Property getDeviceIdentifierSampleProperty() { + return GarminHrvSummarySampleDao.Properties.DeviceId; + } + + @Override + public GarminHrvSummarySample createSample() { + return new GarminHrvSummarySample(); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/GarminHrvValueSampleProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/GarminHrvValueSampleProvider.java new file mode 100644 index 000000000..de6d88b10 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/GarminHrvValueSampleProvider.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.GarminHrvValueSample; +import nodomain.freeyourgadget.gadgetbridge.entities.GarminHrvValueSampleDao; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; + +public class GarminHrvValueSampleProvider extends AbstractTimeSampleProvider { + public GarminHrvValueSampleProvider(final GBDevice device, final DaoSession session) { + super(device, session); + } + + @NonNull + @Override + public AbstractDao getSampleDao() { + return getSession().getGarminHrvValueSampleDao(); + } + + @NonNull + @Override + protected Property getTimestampSampleProperty() { + return GarminHrvValueSampleDao.Properties.Timestamp; + } + + @NonNull + @Override + protected Property getDeviceIdentifierSampleProperty() { + return GarminHrvValueSampleDao.Properties.DeviceId; + } + + @Override + public GarminHrvValueSample createSample() { + return new GarminHrvValueSample(); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/entities/AbstractHrvSummarySample.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/entities/AbstractHrvSummarySample.java new file mode 100644 index 000000000..1c4ba5637 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/entities/AbstractHrvSummarySample.java @@ -0,0 +1,50 @@ +/* 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.entities; + +import androidx.annotation.NonNull; + +import nodomain.freeyourgadget.gadgetbridge.model.HrvSummarySample; +import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils; + +public abstract class AbstractHrvSummarySample extends AbstractTimeSample implements HrvSummarySample { + public Integer getStatusNum() { + return Status.NONE.getNum(); + } + + @Override + public Status getStatus() { + return Status.fromNum(getStatusNum()); + } + + @NonNull + @Override + public String toString() { + return getClass().getSimpleName() + "{" + + "timestamp=" + DateTimeUtils.formatDateTime(DateTimeUtils.parseTimestampMillis(getTimestamp())) + + ", weeklyAverage=" + getWeeklyAverage() + + ", lastNightAverage=" + getLastNightAverage() + + ", lastNight5MinHigh=" + getLastNight5MinHigh() + + ", baselineLowUpper=" + getBaselineLowUpper() + + ", baselineBalancedLower=" + getBaselineBalancedLower() + + ", baselineBalancedUpper=" + getBaselineBalancedUpper() + + ", status=" + getStatus() + + ", userId=" + getUserId() + + ", deviceId=" + getDeviceId() + + "}"; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/entities/AbstractHrvValueSample.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/entities/AbstractHrvValueSample.java new file mode 100644 index 000000000..8e80e94ad --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/entities/AbstractHrvValueSample.java @@ -0,0 +1,35 @@ +/* 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.entities; + +import androidx.annotation.NonNull; + +import nodomain.freeyourgadget.gadgetbridge.model.HrvValueSample; +import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils; + +public abstract class AbstractHrvValueSample extends AbstractTimeSample implements HrvValueSample { + @NonNull + @Override + public String toString() { + return getClass().getSimpleName() + "{" + + "timestamp=" + DateTimeUtils.formatDateTime(DateTimeUtils.parseTimestampMillis(getTimestamp())) + + ", value=" + getValue() + + ", userId=" + getUserId() + + ", deviceId=" + getDeviceId() + + "}"; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/HrvSummarySample.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/HrvSummarySample.java new file mode 100644 index 000000000..82a9d17f3 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/HrvSummarySample.java @@ -0,0 +1,80 @@ +/* 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.model; + +public interface HrvSummarySample extends TimeSample { + enum Status { + NONE(0), + POOR(1), + LOW(2), + UNBALANCED(3), + BALANCED(4), + ; + + private final int num; + + Status(final int num) { + this.num = num; + } + + public int getNum() { + return num; + } + + public static Status fromNum(final int num) { + for (Status value : Status.values()) { + if (value.getNum() == num) { + return value; + } + } + + throw new IllegalArgumentException("Unknown hrv status num " + num); + } + } + + /** + * Weekly average, in milliseconds. + */ + Integer getWeeklyAverage(); + + /** + * Last night average, in milliseconds. + */ + Integer getLastNightAverage(); + + /** + * Last night 5-min high, in milliseconds. + */ + Integer getLastNight5MinHigh(); + + /** + * Baseline low upper, in milliseconds. + */ + Integer getBaselineLowUpper(); + + /** + * Baseline balanced lower, in milliseconds. + */ + Integer getBaselineBalancedLower(); + + /** + * Baseline balanced upper, in milliseconds. + */ + Integer getBaselineBalancedUpper(); + + Status getStatus(); +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/HrvValueSample.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/HrvValueSample.java new file mode 100644 index 000000000..153c70b2a --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/HrvValueSample.java @@ -0,0 +1,24 @@ +/* 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.model; + +public interface HrvValueSample extends TimeSample { + /** + * HRV value, in milliseconds. + */ + int getValue(); +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/FieldDefinitionFactory.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/FieldDefinitionFactory.java index 497d3c637..640a95d0c 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/FieldDefinitionFactory.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/FieldDefinitionFactory.java @@ -6,6 +6,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefi import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionFileType; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionGoalSource; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionGoalType; +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionHrvStatus; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionLanguage; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionMeasurementSystem; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionSleepStage; @@ -30,6 +31,8 @@ public class FieldDefinitionFactory { return new FieldDefinitionGoalSource(localNumber, size, baseType, name); case GOAL_TYPE: return new FieldDefinitionGoalType(localNumber, size, baseType, name); + case HRV_STATUS: + return new FieldDefinitionHrvStatus(localNumber, size, baseType, name); case MEASUREMENT_SYSTEM: return new FieldDefinitionMeasurementSystem(localNumber, size, baseType, name); case TEMPERATURE: @@ -55,6 +58,7 @@ public class FieldDefinitionFactory { FILE_TYPE, GOAL_SOURCE, GOAL_TYPE, + HRV_STATUS, MEASUREMENT_SYSTEM, TEMPERATURE, TIMESTAMP, 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 36e6edf4c..dec55c37c 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 @@ -32,6 +32,8 @@ import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; import nodomain.freeyourgadget.gadgetbridge.database.DBHelper; import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminActivitySampleProvider; import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminEventSampleProvider; +import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminHrvSummarySampleProvider; +import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminHrvValueSampleProvider; import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminSleepStageSampleProvider; import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminSpo2SampleProvider; import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminStressSampleProvider; @@ -40,6 +42,8 @@ import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; import nodomain.freeyourgadget.gadgetbridge.entities.Device; import nodomain.freeyourgadget.gadgetbridge.entities.GarminActivitySample; import nodomain.freeyourgadget.gadgetbridge.entities.GarminEventSample; +import nodomain.freeyourgadget.gadgetbridge.entities.GarminHrvSummarySample; +import nodomain.freeyourgadget.gadgetbridge.entities.GarminHrvValueSample; import nodomain.freeyourgadget.gadgetbridge.entities.GarminSleepStageSample; import nodomain.freeyourgadget.gadgetbridge.entities.GarminSpo2Sample; import nodomain.freeyourgadget.gadgetbridge.entities.GarminStressSample; @@ -51,9 +55,12 @@ import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample; import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryData; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.GarminTimeUtils; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.enums.GarminSport; +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.messages.FitEvent; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitFileId; +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitHrvSummary; +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitHrvValue; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitMonitoring; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitRecord; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitSession; @@ -75,6 +82,8 @@ public class FitImporter { private final List spo2samples = new ArrayList<>(); private final List events = new ArrayList<>(); private final List sleepStageSamples = new ArrayList<>(); + private final List hrvSummarySamples = new ArrayList<>(); + private final List hrvValueSamples = new ArrayList<>(); private final List timesInZone = new ArrayList<>(); private final List activityPoints = new ArrayList<>(); private final Map unknownRecords = new HashMap<>(); @@ -179,6 +188,34 @@ public class FitImporter { } else if (record instanceof FitTimeInZone) { LOG.trace("Time in zone: {}", record); timesInZone.add((FitTimeInZone) record); + } else if (record instanceof FitHrvSummary) { + final FitHrvSummary hrvSummary = (FitHrvSummary) record; + final FieldDefinitionHrvStatus.HrvStatus status = hrvSummary.getStatus(); + if (status == null) { + continue; + } + LOG.trace("HRV summary at {}: {}", ts, record); + final GarminHrvSummarySample sample = new GarminHrvSummarySample( ); + sample.setTimestamp(ts * 1000L); + sample.setWeeklyAverage(hrvSummary.getWeeklyAverage()); + sample.setLastNightAverage(hrvSummary.getLastNightAverage()); + sample.setLastNight5MinHigh(hrvSummary.getLastNight5MinHigh()); + sample.setBaselineLowUpper(hrvSummary.getBaselineLowUpper()); + sample.setBaselineBalancedLower(hrvSummary.getBaselineBalancedLower()); + sample.setBaselineBalancedUpper(hrvSummary.getBaselineBalancedUpper()); + sample.setStatusNum(status.getId()); + hrvSummarySamples.add(sample); + } else if (record instanceof FitHrvValue) { + final FitHrvValue hrvValue = (FitHrvValue) record; + if (hrvValue.getValue() == null) { + LOG.warn("HRV value at {} is null", ts); + continue; + } + LOG.trace("HRV value at {}: {}", ts, hrvValue.getValue()); + final GarminHrvValueSample sample = new GarminHrvValueSample(); + sample.setTimestamp(ts * 1000L); + sample.setValue(hrvValue.getValue()); + hrvValueSamples.add(sample); } else { LOG.trace("Unknown record: {}", record); @@ -202,18 +239,22 @@ public class FitImporter { } switch (fileId.getType()) { - case activity: + case ACTIVITY: persistWorkout(file); break; - case monitor: + case MONITOR: persistActivitySamples(); persistSpo2Samples(); persistStressSamples(); break; - case sleep: + case SLEEP: persistEvents(); persistSleepStageSamples(); break; + case HRV_STATUS: + persistHrvSummarySamples(); + persistHrvValueSamples(); + break; default: LOG.warn("Unable to handle fit file of type {}", fileId.getType()); } @@ -512,6 +553,58 @@ public class FitImporter { } } + private void persistHrvSummarySamples() { + if (hrvSummarySamples.isEmpty()) { + return; + } + + LOG.debug("Will persist {} HRV summary samples", hrvSummarySamples.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 GarminHrvSummarySampleProvider sampleProvider = new GarminHrvSummarySampleProvider(gbDevice, session); + + for (final GarminHrvSummarySample sample : hrvSummarySamples) { + sample.setDevice(device); + sample.setUser(user); + } + + sampleProvider.addSamples(hrvSummarySamples); + } 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; 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 ce021ccf0..4399745ce 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 @@ -253,6 +253,22 @@ public class GlobalFITMessage { public static GlobalFITMessage SLEEP_STATS = new GlobalFITMessage(346, "SLEEP_STATS", Arrays.asList( )); + public static GlobalFITMessage HRV_SUMMARY = new GlobalFITMessage(370, "HRV_SUMMARY", Arrays.asList( + new FieldDefinitionPrimitive(0, BaseType.UINT16, "weekly_average", 128, 0), // milliseconds, scaled by 128 + new FieldDefinitionPrimitive(1, BaseType.UINT16, "last_night_average", 128, 0), // milliseconds, scaled by 128 + new FieldDefinitionPrimitive(2, BaseType.UINT16, "last_night_5_min_high", 128, 0), // milliseconds, scaled by 128 + new FieldDefinitionPrimitive(3, BaseType.UINT16, "baseline_low_upper", 128, 0), // milliseconds, scaled by 128 + new FieldDefinitionPrimitive(4, BaseType.UINT16, "baseline_balanced_lower", 128, 0), // milliseconds, scaled by 128 + new FieldDefinitionPrimitive(5, BaseType.UINT16, "baseline_balanced_upper", 128, 0), // milliseconds, scaled by 128 + new FieldDefinitionPrimitive(6, BaseType.ENUM, "status", FieldDefinitionFactory.FIELD.HRV_STATUS), + new FieldDefinitionPrimitive(253, BaseType.UINT32, "timestamp", FieldDefinitionFactory.FIELD.TIMESTAMP) + )); + + public static GlobalFITMessage HRV_VALUE = new GlobalFITMessage(371, "HRV_VALUE", Arrays.asList( + new FieldDefinitionPrimitive(0, BaseType.UINT16, "value", 128, 0), // milliseconds, scaled by 128 + new FieldDefinitionPrimitive(253, BaseType.UINT32, "timestamp", FieldDefinitionFactory.FIELD.TIMESTAMP) + )); + public static Map KNOWN_MESSAGES = new HashMap() {{ put(0, FILE_ID); put(2, DEVICE_SETTINGS); @@ -280,6 +296,8 @@ public class GlobalFITMessage { put(275, SLEEP_STAGE); put(297, RESPIRATION_RATE); put(346, SLEEP_STATS); + put(370, HRV_SUMMARY); + put(371, HRV_VALUE); }}; private final int number; private final String name; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/codegen/FitCodeGen.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/codegen/FitCodeGen.java index a3caa5d34..e324bea48 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/codegen/FitCodeGen.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/codegen/FitCodeGen.java @@ -22,13 +22,14 @@ import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.FileType; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.GlobalFITMessage; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordData; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordDefinition; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordHeader; -import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionFileType; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionGoalSource; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionGoalType; +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionHrvStatus; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionLanguage; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionMeasurementSystem; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionSleepStage; @@ -230,11 +231,13 @@ public class FitCodeGen { case DAY_OF_WEEK: return DayOfWeek.class; case FILE_TYPE: - return FieldDefinitionFileType.Type.class; + return FileType.FILETYPE.class; case GOAL_SOURCE: return FieldDefinitionGoalSource.Source.class; case GOAL_TYPE: return FieldDefinitionGoalType.Type.class; + case HRV_STATUS: + return FieldDefinitionHrvStatus.HrvStatus.class; case MEASUREMENT_SYSTEM: return FieldDefinitionMeasurementSystem.Type.class; case TEMPERATURE: diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/fieldDefinitions/FieldDefinitionFileType.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/fieldDefinitions/FieldDefinitionFileType.java index d443647b7..2d21453b6 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/fieldDefinitions/FieldDefinitionFileType.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/fieldDefinitions/FieldDefinitionFileType.java @@ -1,9 +1,8 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions; -import androidx.annotation.Nullable; - import java.nio.ByteBuffer; +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.FileType; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.FieldDefinition; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.baseTypes.BaseType; @@ -16,47 +15,16 @@ public class FieldDefinitionFileType extends FieldDefinition { @Override public Object decode(ByteBuffer byteBuffer) { int raw = (int) baseType.decode(byteBuffer, scale, offset); - return Type.fromId(raw) == null ? raw : Type.fromId(raw); + final FileType.FILETYPE fileType = FileType.FILETYPE.fromDataTypeSubType(128, raw); + return fileType == null ? raw : fileType; } @Override public void encode(ByteBuffer byteBuffer, Object o) { - if (o instanceof Type) { - baseType.encode(byteBuffer, (((Type) o).getId()), scale, offset); + if (o instanceof FileType.FILETYPE) { + baseType.encode(byteBuffer, (((FileType.FILETYPE) o).getSubType()), scale, offset); return; } baseType.encode(byteBuffer, o, scale, offset); } - - public enum Type { - settings(2), - activity(4), //FIT_TYPE_4 stands for activity directory - goals(11), - monitor(32), //FIT_TYPE_32 - changelog(41), // FIT_TYPE_41 stands for changelog directory - metrics(44), //FIT_TYPE_41 - sleep(49), //FIT_TYPE_49 - ; - - private final int id; - - Type(int i) { - this.id = i; - } - - @Nullable - public static Type fromId(int id) { - for (Type type : - Type.values()) { - if (id == type.getId()) { - return type; - } - } - return null; - } - - public int getId() { - return this.id; - } - } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/fieldDefinitions/FieldDefinitionHrvStatus.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/fieldDefinitions/FieldDefinitionHrvStatus.java new file mode 100644 index 000000000..459154c65 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/fieldDefinitions/FieldDefinitionHrvStatus.java @@ -0,0 +1,58 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions; + +import androidx.annotation.Nullable; + +import java.nio.ByteBuffer; + +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.FieldDefinition; +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.baseTypes.BaseType; + +public class FieldDefinitionHrvStatus extends FieldDefinition { + public FieldDefinitionHrvStatus(final int localNumber, final int size, final BaseType baseType, final String name) { + super(localNumber, size, baseType, name, 1, 0); + } + + @Override + public Object decode(final ByteBuffer byteBuffer) { + final int raw = (int) baseType.decode(byteBuffer, scale, offset); + return HrvStatus.fromId(raw); + } + + @Override + public void encode(final ByteBuffer byteBuffer, final Object o) { + if (o instanceof HrvStatus) { + baseType.encode(byteBuffer, (((HrvStatus) o).getId()), scale, offset); + return; + } + baseType.encode(byteBuffer, o, scale, offset); + } + + public enum HrvStatus { + NONE(0), + POOR(1), + LOW(2), + UNBALANCED(3), + BALANCED(4), + ; + + private final int id; + + HrvStatus(final int i) { + id = i; + } + + @Nullable + public static HrvStatus fromId(final int id) { + for (HrvStatus stage : HrvStatus.values()) { + if (id == stage.getId()) { + return stage; + } + } + return null; + } + + public int getId() { + return id; + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/messages/FitFileId.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/messages/FitFileId.java index 191e658fb..01c89058d 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/messages/FitFileId.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/messages/FitFileId.java @@ -2,10 +2,10 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages import androidx.annotation.Nullable; +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.FileType.FILETYPE; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordData; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordDefinition; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordHeader; -import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionFileType.Type; // // WARNING: This class was auto-generated, please avoid modifying it directly. @@ -22,8 +22,8 @@ public class FitFileId extends RecordData { } @Nullable - public Type getType() { - return (Type) getFieldByNumber(0); + public FILETYPE getType() { + return (FILETYPE) getFieldByNumber(0); } @Nullable diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/messages/FitHrvSummary.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/messages/FitHrvSummary.java new file mode 100644 index 000000000..ea6023428 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/messages/FitHrvSummary.java @@ -0,0 +1,63 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages; + +import androidx.annotation.Nullable; + +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordData; +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordDefinition; +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordHeader; +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionHrvStatus.HrvStatus; + +// +// WARNING: This class was auto-generated, please avoid modifying it directly. +// See nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.codegen.FitCodeGen +// +public class FitHrvSummary extends RecordData { + public FitHrvSummary(final RecordDefinition recordDefinition, final RecordHeader recordHeader) { + super(recordDefinition, recordHeader); + + final int globalNumber = recordDefinition.getGlobalFITMessage().getNumber(); + if (globalNumber != 370) { + throw new IllegalArgumentException("FitHrvSummary expects global messages of " + 370 + ", got " + globalNumber); + } + } + + @Nullable + public Integer getWeeklyAverage() { + return (Integer) getFieldByNumber(0); + } + + @Nullable + public Integer getLastNightAverage() { + return (Integer) getFieldByNumber(1); + } + + @Nullable + public Integer getLastNight5MinHigh() { + return (Integer) getFieldByNumber(2); + } + + @Nullable + public Integer getBaselineLowUpper() { + return (Integer) getFieldByNumber(3); + } + + @Nullable + public Integer getBaselineBalancedLower() { + return (Integer) getFieldByNumber(4); + } + + @Nullable + public Integer getBaselineBalancedUpper() { + return (Integer) getFieldByNumber(5); + } + + @Nullable + public HrvStatus getStatus() { + return (HrvStatus) getFieldByNumber(6); + } + + @Nullable + public Long getTimestamp() { + return (Long) getFieldByNumber(253); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/messages/FitHrvValue.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/messages/FitHrvValue.java new file mode 100644 index 000000000..c04d3a57b --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/messages/FitHrvValue.java @@ -0,0 +1,32 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages; + +import androidx.annotation.Nullable; + +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordData; +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordDefinition; +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordHeader; + +// +// WARNING: This class was auto-generated, please avoid modifying it directly. +// See nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.codegen.FitCodeGen +// +public class FitHrvValue extends RecordData { + public FitHrvValue(final RecordDefinition recordDefinition, final RecordHeader recordHeader) { + super(recordDefinition, recordHeader); + + final int globalNumber = recordDefinition.getGlobalFITMessage().getNumber(); + if (globalNumber != 371) { + throw new IllegalArgumentException("FitHrvValue expects global messages of " + 371 + ", got " + globalNumber); + } + } + + @Nullable + public Integer getValue() { + return (Integer) getFieldByNumber(0); + } + + @Nullable + public Long getTimestamp() { + return (Long) getFieldByNumber(253); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/messages/FitRecordDataFactory.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/messages/FitRecordDataFactory.java index 2ee6b976b..0ce3b1821 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/messages/FitRecordDataFactory.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/messages/FitRecordDataFactory.java @@ -67,6 +67,10 @@ public class FitRecordDataFactory { return new FitRespirationRate(recordDefinition, recordHeader); case 346: return new FitSleepStats(recordDefinition, recordHeader); + case 370: + return new FitHrvSummary(recordDefinition, recordHeader); + case 371: + return new FitHrvValue(recordDefinition, recordHeader); } return new RecordData(recordDefinition, recordHeader);