From fb1d0a92cca70d6798d2ee1173baa53bf513b74d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Rebelo?= Date: Fri, 11 Oct 2024 23:51:40 +0100 Subject: [PATCH] Garmin: Add resting heart rate sample provider --- .../gadgetbridge/daogen/GBDaoGenerator.java | 10 +++- .../devices/garmin/GarminCoordinator.java | 5 ++ .../GarminHeartRateRestingSampleProvider.java | 56 +++++++++++++++++++ .../devices/garmin/fit/FitImporter.java | 47 +++++++++++++++- .../devices/garmin/fit/GlobalFITMessage.java | 8 +++ .../fit/messages/FitMonitoringHrData.java | 37 ++++++++++++ .../fit/messages/FitRecordDataFactory.java | 2 + 7 files changed, 163 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/GarminHeartRateRestingSampleProvider.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/messages/FitMonitoringHrData.java diff --git a/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java b/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java index fb6d20ad2..d08559c79 100644 --- a/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java +++ b/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java @@ -46,7 +46,7 @@ public class GBDaoGenerator { public static void main(String[] args) throws Exception { - final Schema schema = new Schema(81, MAIN_PACKAGE + ".entities"); + final Schema schema = new Schema(82, MAIN_PACKAGE + ".entities"); Entity userAttributes = addUserAttributes(schema); Entity user = addUserInfo(schema, userAttributes); @@ -118,6 +118,7 @@ public class GBDaoGenerator { addGarminHrvSummarySample(schema, user, device); addGarminHrvValueSample(schema, user, device); addGarminRespiratoryRateSample(schema, user, device); + addGarminHeartRateRestingSample(schema, user, device); addPendingFile(schema, user, device); addWena3EnergySample(schema, user, device); addWena3BehaviorSample(schema, user, device); @@ -828,6 +829,13 @@ public class GBDaoGenerator { return garminRespiratoryRateSample; } + private static Entity addGarminHeartRateRestingSample(Schema schema, Entity user, Entity device) { + Entity hrRestingSample = addEntity(schema, "GarminHeartRateRestingSample"); + addCommonTimeSampleProperties("AbstractHeartRateSample", hrRestingSample, user, device); + hrRestingSample.addIntProperty(SAMPLE_HEART_RATE).notNull().codeBeforeGetter(OVERRIDE); + return hrRestingSample; + } + 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/garmin/GarminCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/GarminCoordinator.java index 53a4d4866..d3eeef484 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 @@ -154,6 +154,11 @@ public abstract class GarminCoordinator extends AbstractBLEDeviceCoordinator { return new GarminRespiratoryRateSampleProvider(device, session); } + @Override + public GarminHeartRateRestingSampleProvider getHeartRateRestingSampleProvider(final GBDevice device, final DaoSession session) { + return new GarminHeartRateRestingSampleProvider(device, session); + } + @Override public DeviceSpecificSettings getDeviceSpecificSettings(final GBDevice device) { final DeviceSpecificSettings deviceSpecificSettings = new DeviceSpecificSettings(); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/GarminHeartRateRestingSampleProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/GarminHeartRateRestingSampleProvider.java new file mode 100644 index 000000000..15a3779d2 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/GarminHeartRateRestingSampleProvider.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.GarminHeartRateRestingSample; +import nodomain.freeyourgadget.gadgetbridge.entities.GarminHeartRateRestingSampleDao; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; + +public class GarminHeartRateRestingSampleProvider extends AbstractTimeSampleProvider { + public GarminHeartRateRestingSampleProvider(final GBDevice device, final DaoSession session) { + super(device, session); + } + + @NonNull + @Override + public AbstractDao getSampleDao() { + return getSession().getGarminHeartRateRestingSampleDao(); + } + + @NonNull + @Override + protected Property getTimestampSampleProperty() { + return GarminHeartRateRestingSampleDao.Properties.Timestamp; + } + + @NonNull + @Override + protected Property getDeviceIdentifierSampleProperty() { + return GarminHeartRateRestingSampleDao.Properties.DeviceId; + } + + @Override + public GarminHeartRateRestingSample createSample() { + return new GarminHeartRateRestingSample(); + } +} 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 734c6844b..54703bb71 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 @@ -22,6 +22,7 @@ import nodomain.freeyourgadget.gadgetbridge.database.DBHelper; import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminActivitySampleProvider; import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminBodyEnergySampleProvider; import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminEventSampleProvider; +import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminHeartRateRestingSampleProvider; import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminHrvSummarySampleProvider; import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminHrvValueSampleProvider; import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminRespiratoryRateSampleProvider; @@ -35,6 +36,7 @@ import nodomain.freeyourgadget.gadgetbridge.entities.Device; import nodomain.freeyourgadget.gadgetbridge.entities.GarminActivitySample; import nodomain.freeyourgadget.gadgetbridge.entities.GarminBodyEnergySample; import nodomain.freeyourgadget.gadgetbridge.entities.GarminEventSample; +import nodomain.freeyourgadget.gadgetbridge.entities.GarminHeartRateRestingSample; import nodomain.freeyourgadget.gadgetbridge.entities.GarminHrvSummarySample; import nodomain.freeyourgadget.gadgetbridge.entities.GarminHrvValueSample; import nodomain.freeyourgadget.gadgetbridge.entities.GarminRespiratoryRateSample; @@ -54,6 +56,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages. 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.FitMonitoringHrData; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitPhysiologicalMetrics; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitRecord; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitRespirationRate; @@ -78,6 +81,7 @@ public class FitImporter { private final List bodyEnergySamples = new ArrayList<>(); private final List spo2samples = new ArrayList<>(); private final List respiratoryRateSamples = new ArrayList<>(); + private final List restingHrSamples = new ArrayList<>(); private final List events = new ArrayList<>(); private final List sleepStageSamples = new ArrayList<>(); private final List hrvSummarySamples = new ArrayList<>(); @@ -95,7 +99,9 @@ public class FitImporter { this.workoutParser = new GarminWorkoutParser(context); } - /** @noinspection StatementWithEmptyBody*/ + /** + * @noinspection StatementWithEmptyBody + */ public void importFile(final File file) throws IOException { reset(); @@ -258,6 +264,17 @@ public class FitImporter { sample.setTimestamp(ts * 1000L); sample.setValue(Math.round(hrvValue.getValue())); hrvValueSamples.add(sample); + } else if (record instanceof FitMonitoringHrData) { + final FitMonitoringHrData monitoringHrData = (FitMonitoringHrData) record; + if (monitoringHrData.getRestingHeartRate() == null) { + LOG.warn("Resting HR at {} is null", ts); + continue; + } + LOG.trace("Resting HR at {}: {}", ts, monitoringHrData.getRestingHeartRate()); + final GarminHeartRateRestingSample sample = new GarminHeartRateRestingSample(); + sample.setTimestamp(ts * 1000L); + sample.setHeartRate(monitoringHrData.getRestingHeartRate()); + restingHrSamples.add(sample); } else { LOG.trace("Unknown record: {}", record); @@ -288,6 +305,7 @@ public class FitImporter { persistActivitySamples(); persistSpo2Samples(); persistRespiratoryRateSamples(); + persistRestingHrSamples(); persistStressSamples(); persistBodyEnergySamples(); break; @@ -351,6 +369,7 @@ public class FitImporter { bodyEnergySamples.clear(); spo2samples.clear(); respiratoryRateSamples.clear(); + restingHrSamples.clear(); events.clear(); sleepStageSamples.clear(); hrvSummarySamples.clear(); @@ -705,6 +724,32 @@ public class FitImporter { } } + 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; 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 82c3ac2de..7322c6e58 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 @@ -269,6 +269,12 @@ public class GlobalFITMessage { new FieldDefinitionPrimitive(3, BaseType.UINT8, "developer_data_index") )); + public static GlobalFITMessage MONITORING_HR_DATA = new GlobalFITMessage(211, "MONITORING_HR_DATA", Arrays.asList( + new FieldDefinitionPrimitive(0, BaseType.UINT8, "resting_heart_rate"), + new FieldDefinitionPrimitive(1, BaseType.UINT8, "current_day_resting_heart_rate"), + new FieldDefinitionPrimitive(253, BaseType.UINT32, "timestamp", FieldDefinitionFactory.FIELD.TIMESTAMP) + )); + public static GlobalFITMessage TIME_IN_ZONE = new GlobalFITMessage(216, "TIME_IN_ZONE", Arrays.asList( new FieldDefinitionPrimitive(0, BaseType.UINT16, "reference_message"), new FieldDefinitionPrimitive(1, BaseType.UINT16, "reference_index"), @@ -388,6 +394,7 @@ public class GlobalFITMessage { put(162, TIMESTAMP_CORRELATION); put(206, FIELD_DESCRIPTION); put(207, DEVELOPER_DATA); + put(211, MONITORING_HR_DATA); put(216, TIME_IN_ZONE); put(222, ALARM_SETTINGS); put(225, SET); @@ -403,6 +410,7 @@ public class GlobalFITMessage { put(397, SKIN_TEMP_RAW); put(398, SKIN_TEMP_OVERNIGHT); }}; + private final int number; private final String name; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/messages/FitMonitoringHrData.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/messages/FitMonitoringHrData.java new file mode 100644 index 000000000..9dc337890 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/messages/FitMonitoringHrData.java @@ -0,0 +1,37 @@ +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 FitMonitoringHrData extends RecordData { + public FitMonitoringHrData(final RecordDefinition recordDefinition, final RecordHeader recordHeader) { + super(recordDefinition, recordHeader); + + final int globalNumber = recordDefinition.getGlobalFITMessage().getNumber(); + if (globalNumber != 211) { + throw new IllegalArgumentException("FitMonitoringHrData expects global messages of " + 211 + ", got " + globalNumber); + } + } + + @Nullable + public Integer getRestingHeartRate() { + return (Integer) getFieldByNumber(0); + } + + @Nullable + public Integer getCurrentDayRestingHeartRate() { + return (Integer) getFieldByNumber(1); + } + + @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 7f0063b8f..9eb722085 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 @@ -61,6 +61,8 @@ public class FitRecordDataFactory { return new FitFieldDescription(recordDefinition, recordHeader); case 207: return new FitDeveloperData(recordDefinition, recordHeader); + case 211: + return new FitMonitoringHrData(recordDefinition, recordHeader); case 216: return new FitTimeInZone(recordDefinition, recordHeader); case 222: