From 61e2411081ff1698a2277f6e7fa80dda1c836c7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Rebelo?= Date: Sun, 4 Aug 2024 22:59:29 +0100 Subject: [PATCH] Garmin: Parse and persist body energy --- .../gadgetbridge/daogen/GBDaoGenerator.java | 16 +++-- .../devices/AbstractDeviceCoordinator.java | 11 ++++ .../devices/DeviceCoordinator.java | 8 +++ .../GarminBodyEnergySampleProvider.java | 56 ++++++++++++++++++ .../devices/garmin/GarminCoordinator.java | 11 ++++ .../entities/AbstractBodyEnergySample.java | 36 ++++++++++++ .../gadgetbridge/model/BodyEnergySample.java | 24 ++++++++ .../devices/garmin/fit/FitImporter.java | 58 ++++++++++++++++--- .../devices/garmin/fit/GlobalFITMessage.java | 3 +- .../garmin/fit/messages/FitStressLevel.java | 5 ++ 10 files changed, 215 insertions(+), 13 deletions(-) create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/GarminBodyEnergySampleProvider.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/entities/AbstractBodyEnergySample.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/BodyEnergySample.java diff --git a/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java b/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java index 2e8b101d6..b8cf1ac5a 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(75, MAIN_PACKAGE + ".entities"); + final Schema schema = new Schema(76, MAIN_PACKAGE + ".entities"); Entity userAttributes = addUserAttributes(schema); Entity user = addUserInfo(schema, userAttributes); @@ -97,7 +97,7 @@ public class GBDaoGenerator { addWatchXPlusHealthActivityKindOverlay(schema, user, device); addTLW64ActivitySample(schema, user, device); addLefunActivitySample(schema, user, device); - addLefunBiometricSample(schema,user,device); + addLefunBiometricSample(schema, user, device); addLefunSleepSample(schema, user, device); addSonySWR12Sample(schema, user, device); addBangleJSActivitySample(schema, user, device); @@ -110,6 +110,7 @@ public class GBDaoGenerator { addGarminFitFile(schema, user, device); addGarminActivitySample(schema, user, device); addGarminStressSample(schema, user, device); + addGarminBodyEnergySample(schema, user, device); addGarminSpo2Sample(schema, user, device); addGarminSleepStageSample(schema, user, device); addGarminEventSample(schema, user, device); @@ -632,7 +633,7 @@ public class GBDaoGenerator { return activitySample; } - private static Entity addCyclingSample(Schema schema, Entity user, Entity device){ + private static Entity addCyclingSample(Schema schema, Entity user, Entity device) { Entity cyclingSample = addEntity(schema, "CyclingSample"); addCommonTimeSampleProperties("AbstractTimeSample", cyclingSample, user, device); @@ -704,6 +705,13 @@ public class GBDaoGenerator { return stressSample; } + private static Entity addGarminBodyEnergySample(Schema schema, Entity user, Entity device) { + Entity stressSample = addEntity(schema, "GarminBodyEnergySample"); + addCommonTimeSampleProperties("AbstractBodyEnergySample", stressSample, user, device); + stressSample.addIntProperty("energy").notNull().codeBeforeGetter(OVERRIDE); + return stressSample; + } + private static Entity addGarminSpo2Sample(Schema schema, Entity user, Entity device) { Entity spo2sample = addEntity(schema, "GarminSpo2Sample"); addCommonTimeSampleProperties("AbstractSpo2Sample", spo2sample, user, device); @@ -798,7 +806,7 @@ public class GBDaoGenerator { return activitySample; } - private static Entity addCasioGBX100Sample(Schema schema, Entity user, Entity device) { + private static Entity addCasioGBX100Sample(Schema schema, Entity user, Entity device) { Entity activitySample = addEntity(schema, "CasioGBX100ActivitySample"); activitySample.implementsSerializable(); addCommonActivitySampleProperties("AbstractGBX100ActivitySample", activitySample, user, device); 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 a229354b7..fe44123a3 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractDeviceCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractDeviceCoordinator.java @@ -71,6 +71,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.AbstractNotificationPattern; import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample; import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser; import nodomain.freeyourgadget.gadgetbridge.model.BatteryConfig; +import nodomain.freeyourgadget.gadgetbridge.model.BodyEnergySample; import nodomain.freeyourgadget.gadgetbridge.model.DeviceType; import nodomain.freeyourgadget.gadgetbridge.model.HeartRateSample; import nodomain.freeyourgadget.gadgetbridge.model.HrvSummarySample; @@ -210,6 +211,11 @@ public abstract class AbstractDeviceCoordinator implements DeviceCoordinator { return null; } + @Override + public TimeSampleProvider getBodyEnergySampleProvider(final GBDevice device, final DaoSession session) { + return null; + } + @Override public TimeSampleProvider getHrvSummarySampleProvider(GBDevice device, DaoSession session) { return null; @@ -454,6 +460,11 @@ public abstract class AbstractDeviceCoordinator implements DeviceCoordinator { return false; } + @Override + public boolean supportsBodyEnergy() { + return false; + } + @Override public boolean supportsHrvMeasurement() { return false; 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 fde5a3f18..e25dc7ab2 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceCoordinator.java @@ -50,6 +50,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.AbstractNotificationPattern; import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample; import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser; import nodomain.freeyourgadget.gadgetbridge.model.BatteryConfig; +import nodomain.freeyourgadget.gadgetbridge.model.BodyEnergySample; import nodomain.freeyourgadget.gadgetbridge.model.DeviceType; import nodomain.freeyourgadget.gadgetbridge.model.HeartRateSample; import nodomain.freeyourgadget.gadgetbridge.model.HrvSummarySample; @@ -216,6 +217,8 @@ public interface DeviceCoordinator { */ boolean supportsStressMeasurement(); + boolean supportsBodyEnergy(); + boolean supportsHrvMeasurement(); boolean supportsSleepMeasurement(); @@ -287,6 +290,11 @@ public interface DeviceCoordinator { */ TimeSampleProvider getStressSampleProvider(GBDevice device, DaoSession session); + /** + * Returns the sample provider for body energy data, for the device being supported. + */ + TimeSampleProvider getBodyEnergySampleProvider(GBDevice device, DaoSession session); + /** * Returns the sample provider for HRV summary, for the device being supported. */ diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/GarminBodyEnergySampleProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/GarminBodyEnergySampleProvider.java new file mode 100644 index 000000000..1fe89f0ca --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/GarminBodyEnergySampleProvider.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.GarminBodyEnergySample; +import nodomain.freeyourgadget.gadgetbridge.entities.GarminBodyEnergySampleDao; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; + +public class GarminBodyEnergySampleProvider extends AbstractTimeSampleProvider { + public GarminBodyEnergySampleProvider(final GBDevice device, final DaoSession session) { + super(device, session); + } + + @NonNull + @Override + public AbstractDao getSampleDao() { + return getSession().getGarminBodyEnergySampleDao(); + } + + @NonNull + @Override + protected Property getTimestampSampleProperty() { + return GarminBodyEnergySampleDao.Properties.Timestamp; + } + + @NonNull + @Override + protected Property getDeviceIdentifierSampleProperty() { + return GarminBodyEnergySampleDao.Properties.DeviceId; + } + + @Override + public GarminBodyEnergySample createSample() { + return new GarminBodyEnergySample(); + } +} 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 546ca95f6..d053daa08 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,7 @@ 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.BodyEnergySample; import nodomain.freeyourgadget.gadgetbridge.model.HrvSummarySample; import nodomain.freeyourgadget.gadgetbridge.model.HrvValueSample; import nodomain.freeyourgadget.gadgetbridge.model.Spo2Sample; @@ -98,6 +99,11 @@ public abstract class GarminCoordinator extends AbstractBLEDeviceCoordinator { return new GarminStressSampleProvider(device, session); } + @Override + public TimeSampleProvider getBodyEnergySampleProvider(final GBDevice device, final DaoSession session) { + return new GarminBodyEnergySampleProvider(device, session); + } + @Override public TimeSampleProvider getHrvSummarySampleProvider(final GBDevice device, final DaoSession session) { return new GarminHrvSummarySampleProvider(device, session); @@ -174,6 +180,11 @@ public abstract class GarminCoordinator extends AbstractBLEDeviceCoordinator { return true; } + @Override + public boolean supportsBodyEnergy() { + return true; + } + @Override public boolean supportsHrvMeasurement() { return true; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/entities/AbstractBodyEnergySample.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/entities/AbstractBodyEnergySample.java new file mode 100644 index 000000000..6c0bcee7a --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/entities/AbstractBodyEnergySample.java @@ -0,0 +1,36 @@ +/* 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.BodyEnergySample; +import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils; + +public abstract class AbstractBodyEnergySample extends AbstractTimeSample implements BodyEnergySample { + + @NonNull + @Override + public String toString() { + return getClass().getSimpleName() + "{" + + "timestamp=" + DateTimeUtils.formatDateTime(DateTimeUtils.parseTimestampMillis(getTimestamp())) + + ", energy=" + getEnergy() + + ", userId=" + getUserId() + + ", deviceId=" + getDeviceId() + + "}"; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/BodyEnergySample.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/BodyEnergySample.java new file mode 100644 index 000000000..50a412ae4 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/BodyEnergySample.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 BodyEnergySample extends TimeSample { + /** + * Body energy value, between 0 and 100. + */ + int getEnergy(); +} 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 be7096053..62eac383c 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 @@ -31,6 +31,7 @@ import nodomain.freeyourgadget.gadgetbridge.GBApplication; 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.GarminBodyEnergySampleProvider; import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminEventSampleProvider; import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminHrvSummarySampleProvider; import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminHrvValueSampleProvider; @@ -41,6 +42,7 @@ import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary; import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; 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.GarminHrvSummarySample; import nodomain.freeyourgadget.gadgetbridge.entities.GarminHrvValueSample; @@ -79,6 +81,7 @@ public class FitImporter { private final SortedMap> activitySamplesPerTimestamp = new TreeMap<>(); private final List stressSamples = new ArrayList<>(); + private final List bodyEnergySamples = new ArrayList<>(); private final List spo2samples = new ArrayList<>(); private final List events = new ArrayList<>(); private final List sleepStageSamples = new ArrayList<>(); @@ -113,15 +116,24 @@ public class FitImporter { } fileId = newFileId; } else if (record instanceof FitStressLevel) { - final Integer stress = ((FitStressLevel) record).getStressLevelValue(); - if (stress == null || stress < 0) { - continue; + final FitStressLevel stressRecord = (FitStressLevel) record; + final Integer stress = stressRecord.getStressLevelValue(); + if (stress != null && stress >= 0) { + LOG.trace("Stress at {}: {}", ts, stress); + final GarminStressSample sample = new GarminStressSample(); + sample.setTimestamp(ts * 1000L); + sample.setStress(stress); + stressSamples.add(sample); + } + + final Integer energy = stressRecord.getBodyEnergy(); + if (energy != null) { + LOG.trace("Body energy at {}: {}", ts, energy); + final GarminBodyEnergySample sample = new GarminBodyEnergySample(); + sample.setTimestamp(ts * 1000L); + sample.setEnergy(energy); + bodyEnergySamples.add(sample); } - LOG.trace("Stress at {}: {}", ts, stress); - final GarminStressSample sample = new GarminStressSample(); - sample.setTimestamp(ts * 1000L); - sample.setStress(stress); - stressSamples.add(sample); } else if (record instanceof FitSleepStage) { final FieldDefinitionSleepStage.SleepStage stage = ((FitSleepStage) record).getSleepStage(); if (stage == null) { @@ -245,6 +257,7 @@ public class FitImporter { persistActivitySamples(); persistSpo2Samples(); persistStressSamples(); + persistBodyEnergySamples(); break; case SLEEP: persistEvents(); @@ -397,9 +410,12 @@ public class FitImporter { private void reset() { activitySamplesPerTimestamp.clear(); stressSamples.clear(); + bodyEnergySamples.clear(); spo2samples.clear(); events.clear(); sleepStageSamples.clear(); + hrvSummarySamples.clear(); + hrvValueSamples.clear(); timesInZone.clear(); activityPoints.clear(); unknownRecords.clear(); @@ -655,4 +671,30 @@ public class FitImporter { 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); + } + } } 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 4399745ce..3b5916796 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 @@ -230,7 +230,8 @@ public class GlobalFITMessage { public static GlobalFITMessage STRESS_LEVEL = new GlobalFITMessage(227, "STRESS_LEVEL", Arrays.asList( new FieldDefinitionPrimitive(0, BaseType.SINT16, "stress_level_value"), - new FieldDefinitionPrimitive(1, BaseType.UINT32, "stress_level_time", FieldDefinitionFactory.FIELD.TIMESTAMP) + new FieldDefinitionPrimitive(1, BaseType.UINT32, "stress_level_time", FieldDefinitionFactory.FIELD.TIMESTAMP), + new FieldDefinitionPrimitive(3, BaseType.SINT8, "body_energy") )); public static GlobalFITMessage SPO2 = new GlobalFITMessage(269, "SPO2", Arrays.asList( diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/messages/FitStressLevel.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/messages/FitStressLevel.java index 51f9986e4..a588da4d4 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/messages/FitStressLevel.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/messages/FitStressLevel.java @@ -46,6 +46,11 @@ public class FitStressLevel extends RecordData { return (Long) getFieldByNumber(1); } + @Nullable + public Integer getBodyEnergy() { + return (Integer) getFieldByNumber(3); + } + // manual changes below @Override