Garmin: Add resting heart rate sample provider

This commit is contained in:
José Rebelo 2024-10-11 23:51:40 +01:00
parent b331c53763
commit fb1d0a92cc
7 changed files with 163 additions and 2 deletions

View File

@ -46,7 +46,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(81, MAIN_PACKAGE + ".entities"); final Schema schema = new Schema(82, MAIN_PACKAGE + ".entities");
Entity userAttributes = addUserAttributes(schema); Entity userAttributes = addUserAttributes(schema);
Entity user = addUserInfo(schema, userAttributes); Entity user = addUserInfo(schema, userAttributes);
@ -118,6 +118,7 @@ public class GBDaoGenerator {
addGarminHrvSummarySample(schema, user, device); addGarminHrvSummarySample(schema, user, device);
addGarminHrvValueSample(schema, user, device); addGarminHrvValueSample(schema, user, device);
addGarminRespiratoryRateSample(schema, user, device); addGarminRespiratoryRateSample(schema, user, device);
addGarminHeartRateRestingSample(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);
@ -828,6 +829,13 @@ public class GBDaoGenerator {
return garminRespiratoryRateSample; 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) { 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

@ -154,6 +154,11 @@ public abstract class GarminCoordinator extends AbstractBLEDeviceCoordinator {
return new GarminRespiratoryRateSampleProvider(device, session); return new GarminRespiratoryRateSampleProvider(device, session);
} }
@Override
public GarminHeartRateRestingSampleProvider getHeartRateRestingSampleProvider(final GBDevice device, final DaoSession session) {
return new GarminHeartRateRestingSampleProvider(device, session);
}
@Override @Override
public DeviceSpecificSettings getDeviceSpecificSettings(final GBDevice device) { public DeviceSpecificSettings getDeviceSpecificSettings(final GBDevice device) {
final DeviceSpecificSettings deviceSpecificSettings = new DeviceSpecificSettings(); final DeviceSpecificSettings deviceSpecificSettings = new DeviceSpecificSettings();

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.GarminHeartRateRestingSample;
import nodomain.freeyourgadget.gadgetbridge.entities.GarminHeartRateRestingSampleDao;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
public class GarminHeartRateRestingSampleProvider extends AbstractTimeSampleProvider<GarminHeartRateRestingSample> {
public GarminHeartRateRestingSampleProvider(final GBDevice device, final DaoSession session) {
super(device, session);
}
@NonNull
@Override
public AbstractDao<GarminHeartRateRestingSample, ?> 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();
}
}

View File

@ -22,6 +22,7 @@ import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
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;
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminHeartRateRestingSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminHrvSummarySampleProvider; import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminHrvSummarySampleProvider;
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;
@ -35,6 +36,7 @@ import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.entities.GarminActivitySample; import nodomain.freeyourgadget.gadgetbridge.entities.GarminActivitySample;
import nodomain.freeyourgadget.gadgetbridge.entities.GarminBodyEnergySample; import nodomain.freeyourgadget.gadgetbridge.entities.GarminBodyEnergySample;
import nodomain.freeyourgadget.gadgetbridge.entities.GarminEventSample; import nodomain.freeyourgadget.gadgetbridge.entities.GarminEventSample;
import nodomain.freeyourgadget.gadgetbridge.entities.GarminHeartRateRestingSample;
import nodomain.freeyourgadget.gadgetbridge.entities.GarminHrvSummarySample; 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;
@ -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.FitHrvSummary;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitHrvValue; 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.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.FitPhysiologicalMetrics;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitRecord; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitRecord;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitRespirationRate; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitRespirationRate;
@ -78,6 +81,7 @@ public class FitImporter {
private final List<GarminBodyEnergySample> bodyEnergySamples = new ArrayList<>(); private final List<GarminBodyEnergySample> bodyEnergySamples = new ArrayList<>();
private final List<GarminSpo2Sample> spo2samples = new ArrayList<>(); private final List<GarminSpo2Sample> spo2samples = new ArrayList<>();
private final List<GarminRespiratoryRateSample> respiratoryRateSamples = new ArrayList<>(); private final List<GarminRespiratoryRateSample> respiratoryRateSamples = 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<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<>();
@ -95,7 +99,9 @@ public class FitImporter {
this.workoutParser = new GarminWorkoutParser(context); this.workoutParser = new GarminWorkoutParser(context);
} }
/** @noinspection StatementWithEmptyBody*/ /**
* @noinspection StatementWithEmptyBody
*/
public void importFile(final File file) throws IOException { public void importFile(final File file) throws IOException {
reset(); reset();
@ -258,6 +264,17 @@ public class FitImporter {
sample.setTimestamp(ts * 1000L); sample.setTimestamp(ts * 1000L);
sample.setValue(Math.round(hrvValue.getValue())); sample.setValue(Math.round(hrvValue.getValue()));
hrvValueSamples.add(sample); 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 { } else {
LOG.trace("Unknown record: {}", record); LOG.trace("Unknown record: {}", record);
@ -288,6 +305,7 @@ public class FitImporter {
persistActivitySamples(); persistActivitySamples();
persistSpo2Samples(); persistSpo2Samples();
persistRespiratoryRateSamples(); persistRespiratoryRateSamples();
persistRestingHrSamples();
persistStressSamples(); persistStressSamples();
persistBodyEnergySamples(); persistBodyEnergySamples();
break; break;
@ -351,6 +369,7 @@ public class FitImporter {
bodyEnergySamples.clear(); bodyEnergySamples.clear();
spo2samples.clear(); spo2samples.clear();
respiratoryRateSamples.clear(); respiratoryRateSamples.clear();
restingHrSamples.clear();
events.clear(); events.clear();
sleepStageSamples.clear(); sleepStageSamples.clear();
hrvSummarySamples.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() { private void persistStressSamples() {
if (stressSamples.isEmpty()) { if (stressSamples.isEmpty()) {
return; return;

View File

@ -269,6 +269,12 @@ public class GlobalFITMessage {
new FieldDefinitionPrimitive(3, BaseType.UINT8, "developer_data_index") 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( public static GlobalFITMessage TIME_IN_ZONE = new GlobalFITMessage(216, "TIME_IN_ZONE", Arrays.asList(
new FieldDefinitionPrimitive(0, BaseType.UINT16, "reference_message"), new FieldDefinitionPrimitive(0, BaseType.UINT16, "reference_message"),
new FieldDefinitionPrimitive(1, BaseType.UINT16, "reference_index"), new FieldDefinitionPrimitive(1, BaseType.UINT16, "reference_index"),
@ -388,6 +394,7 @@ public class GlobalFITMessage {
put(162, TIMESTAMP_CORRELATION); put(162, TIMESTAMP_CORRELATION);
put(206, FIELD_DESCRIPTION); put(206, FIELD_DESCRIPTION);
put(207, DEVELOPER_DATA); put(207, DEVELOPER_DATA);
put(211, MONITORING_HR_DATA);
put(216, TIME_IN_ZONE); put(216, TIME_IN_ZONE);
put(222, ALARM_SETTINGS); put(222, ALARM_SETTINGS);
put(225, SET); put(225, SET);
@ -403,6 +410,7 @@ public class GlobalFITMessage {
put(397, SKIN_TEMP_RAW); put(397, SKIN_TEMP_RAW);
put(398, SKIN_TEMP_OVERNIGHT); put(398, SKIN_TEMP_OVERNIGHT);
}}; }};
private final int number; private final int number;
private final String name; private final String name;

View File

@ -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);
}
}

View File

@ -61,6 +61,8 @@ public class FitRecordDataFactory {
return new FitFieldDescription(recordDefinition, recordHeader); return new FitFieldDescription(recordDefinition, recordHeader);
case 207: case 207:
return new FitDeveloperData(recordDefinition, recordHeader); return new FitDeveloperData(recordDefinition, recordHeader);
case 211:
return new FitMonitoringHrData(recordDefinition, recordHeader);
case 216: case 216:
return new FitTimeInZone(recordDefinition, recordHeader); return new FitTimeInZone(recordDefinition, recordHeader);
case 222: case 222: