1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2024-12-24 17:45:50 +01:00

Garmin: Parse and persist body energy

This commit is contained in:
José Rebelo 2024-08-04 22:59:29 +01:00
parent e19325fce9
commit 61e2411081
10 changed files with 215 additions and 13 deletions

View File

@ -45,7 +45,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(75, MAIN_PACKAGE + ".entities"); final Schema schema = new Schema(76, MAIN_PACKAGE + ".entities");
Entity userAttributes = addUserAttributes(schema); Entity userAttributes = addUserAttributes(schema);
Entity user = addUserInfo(schema, userAttributes); Entity user = addUserInfo(schema, userAttributes);
@ -97,7 +97,7 @@ public class GBDaoGenerator {
addWatchXPlusHealthActivityKindOverlay(schema, user, device); addWatchXPlusHealthActivityKindOverlay(schema, user, device);
addTLW64ActivitySample(schema, user, device); addTLW64ActivitySample(schema, user, device);
addLefunActivitySample(schema, user, device); addLefunActivitySample(schema, user, device);
addLefunBiometricSample(schema,user,device); addLefunBiometricSample(schema, user, device);
addLefunSleepSample(schema, user, device); addLefunSleepSample(schema, user, device);
addSonySWR12Sample(schema, user, device); addSonySWR12Sample(schema, user, device);
addBangleJSActivitySample(schema, user, device); addBangleJSActivitySample(schema, user, device);
@ -110,6 +110,7 @@ public class GBDaoGenerator {
addGarminFitFile(schema, user, device); addGarminFitFile(schema, user, device);
addGarminActivitySample(schema, user, device); addGarminActivitySample(schema, user, device);
addGarminStressSample(schema, user, device); addGarminStressSample(schema, user, device);
addGarminBodyEnergySample(schema, user, device);
addGarminSpo2Sample(schema, user, device); addGarminSpo2Sample(schema, user, device);
addGarminSleepStageSample(schema, user, device); addGarminSleepStageSample(schema, user, device);
addGarminEventSample(schema, user, device); addGarminEventSample(schema, user, device);
@ -632,7 +633,7 @@ public class GBDaoGenerator {
return activitySample; 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"); Entity cyclingSample = addEntity(schema, "CyclingSample");
addCommonTimeSampleProperties("AbstractTimeSample", cyclingSample, user, device); addCommonTimeSampleProperties("AbstractTimeSample", cyclingSample, user, device);
@ -704,6 +705,13 @@ public class GBDaoGenerator {
return stressSample; 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) { private static Entity addGarminSpo2Sample(Schema schema, Entity user, Entity device) {
Entity spo2sample = addEntity(schema, "GarminSpo2Sample"); Entity spo2sample = addEntity(schema, "GarminSpo2Sample");
addCommonTimeSampleProperties("AbstractSpo2Sample", spo2sample, user, device); addCommonTimeSampleProperties("AbstractSpo2Sample", spo2sample, user, device);
@ -798,7 +806,7 @@ public class GBDaoGenerator {
return activitySample; 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"); Entity activitySample = addEntity(schema, "CasioGBX100ActivitySample");
activitySample.implementsSerializable(); activitySample.implementsSerializable();
addCommonActivitySampleProperties("AbstractGBX100ActivitySample", activitySample, user, device); addCommonActivitySampleProperties("AbstractGBX100ActivitySample", activitySample, user, device);

View File

@ -71,6 +71,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.AbstractNotificationPattern;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample; import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser; import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser;
import nodomain.freeyourgadget.gadgetbridge.model.BatteryConfig; import nodomain.freeyourgadget.gadgetbridge.model.BatteryConfig;
import nodomain.freeyourgadget.gadgetbridge.model.BodyEnergySample;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType; import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
import nodomain.freeyourgadget.gadgetbridge.model.HeartRateSample; import nodomain.freeyourgadget.gadgetbridge.model.HeartRateSample;
import nodomain.freeyourgadget.gadgetbridge.model.HrvSummarySample; import nodomain.freeyourgadget.gadgetbridge.model.HrvSummarySample;
@ -210,6 +211,11 @@ public abstract class AbstractDeviceCoordinator implements DeviceCoordinator {
return null; return null;
} }
@Override
public TimeSampleProvider<? extends BodyEnergySample> getBodyEnergySampleProvider(final GBDevice device, final DaoSession session) {
return null;
}
@Override @Override
public TimeSampleProvider<? extends HrvSummarySample> getHrvSummarySampleProvider(GBDevice device, DaoSession session) { public TimeSampleProvider<? extends HrvSummarySample> getHrvSummarySampleProvider(GBDevice device, DaoSession session) {
return null; return null;
@ -454,6 +460,11 @@ public abstract class AbstractDeviceCoordinator implements DeviceCoordinator {
return false; return false;
} }
@Override
public boolean supportsBodyEnergy() {
return false;
}
@Override @Override
public boolean supportsHrvMeasurement() { public boolean supportsHrvMeasurement() {
return false; return false;

View File

@ -50,6 +50,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.AbstractNotificationPattern;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample; import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser; import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser;
import nodomain.freeyourgadget.gadgetbridge.model.BatteryConfig; import nodomain.freeyourgadget.gadgetbridge.model.BatteryConfig;
import nodomain.freeyourgadget.gadgetbridge.model.BodyEnergySample;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType; import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
import nodomain.freeyourgadget.gadgetbridge.model.HeartRateSample; import nodomain.freeyourgadget.gadgetbridge.model.HeartRateSample;
import nodomain.freeyourgadget.gadgetbridge.model.HrvSummarySample; import nodomain.freeyourgadget.gadgetbridge.model.HrvSummarySample;
@ -216,6 +217,8 @@ public interface DeviceCoordinator {
*/ */
boolean supportsStressMeasurement(); boolean supportsStressMeasurement();
boolean supportsBodyEnergy();
boolean supportsHrvMeasurement(); boolean supportsHrvMeasurement();
boolean supportsSleepMeasurement(); boolean supportsSleepMeasurement();
@ -287,6 +290,11 @@ public interface DeviceCoordinator {
*/ */
TimeSampleProvider<? extends StressSample> getStressSampleProvider(GBDevice device, DaoSession session); TimeSampleProvider<? extends StressSample> getStressSampleProvider(GBDevice device, DaoSession session);
/**
* Returns the sample provider for body energy data, for the device being supported.
*/
TimeSampleProvider<? extends BodyEnergySample> getBodyEnergySampleProvider(GBDevice device, DaoSession session);
/** /**
* Returns the sample provider for HRV summary, for the device being supported. * Returns the sample provider for HRV summary, for the device being supported.
*/ */

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

View File

@ -24,6 +24,7 @@ import nodomain.freeyourgadget.gadgetbridge.entities.GarminSpo2SampleDao;
import nodomain.freeyourgadget.gadgetbridge.entities.GarminStressSampleDao; import nodomain.freeyourgadget.gadgetbridge.entities.GarminStressSampleDao;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample; import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
import nodomain.freeyourgadget.gadgetbridge.model.BodyEnergySample;
import nodomain.freeyourgadget.gadgetbridge.model.HrvSummarySample; import nodomain.freeyourgadget.gadgetbridge.model.HrvSummarySample;
import nodomain.freeyourgadget.gadgetbridge.model.HrvValueSample; import nodomain.freeyourgadget.gadgetbridge.model.HrvValueSample;
import nodomain.freeyourgadget.gadgetbridge.model.Spo2Sample; import nodomain.freeyourgadget.gadgetbridge.model.Spo2Sample;
@ -98,6 +99,11 @@ public abstract class GarminCoordinator extends AbstractBLEDeviceCoordinator {
return new GarminStressSampleProvider(device, session); return new GarminStressSampleProvider(device, session);
} }
@Override
public TimeSampleProvider<? extends BodyEnergySample> getBodyEnergySampleProvider(final GBDevice device, final DaoSession session) {
return new GarminBodyEnergySampleProvider(device, session);
}
@Override @Override
public TimeSampleProvider<? extends HrvSummarySample> getHrvSummarySampleProvider(final GBDevice device, final DaoSession session) { public TimeSampleProvider<? extends HrvSummarySample> getHrvSummarySampleProvider(final GBDevice device, final DaoSession session) {
return new GarminHrvSummarySampleProvider(device, session); return new GarminHrvSummarySampleProvider(device, session);
@ -174,6 +180,11 @@ public abstract class GarminCoordinator extends AbstractBLEDeviceCoordinator {
return true; return true;
} }
@Override
public boolean supportsBodyEnergy() {
return true;
}
@Override @Override
public boolean supportsHrvMeasurement() { public boolean supportsHrvMeasurement() {
return true; return true;

View File

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

View File

@ -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 <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.model;
public interface BodyEnergySample extends TimeSample {
/**
* Body energy value, between 0 and 100.
*/
int getEnergy();
}

View File

@ -31,6 +31,7 @@ import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper; import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminActivitySampleProvider; 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.GarminEventSampleProvider;
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;
@ -41,6 +42,7 @@ import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.Device; import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.entities.GarminActivitySample; import nodomain.freeyourgadget.gadgetbridge.entities.GarminActivitySample;
import nodomain.freeyourgadget.gadgetbridge.entities.GarminBodyEnergySample;
import nodomain.freeyourgadget.gadgetbridge.entities.GarminEventSample; import nodomain.freeyourgadget.gadgetbridge.entities.GarminEventSample;
import nodomain.freeyourgadget.gadgetbridge.entities.GarminHrvSummarySample; import nodomain.freeyourgadget.gadgetbridge.entities.GarminHrvSummarySample;
import nodomain.freeyourgadget.gadgetbridge.entities.GarminHrvValueSample; import nodomain.freeyourgadget.gadgetbridge.entities.GarminHrvValueSample;
@ -79,6 +81,7 @@ public class FitImporter {
private final SortedMap<Integer, List<FitMonitoring>> activitySamplesPerTimestamp = new TreeMap<>(); private final SortedMap<Integer, List<FitMonitoring>> activitySamplesPerTimestamp = new TreeMap<>();
private final List<GarminStressSample> stressSamples = new ArrayList<>(); private final List<GarminStressSample> stressSamples = 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<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<>();
@ -113,15 +116,24 @@ public class FitImporter {
} }
fileId = newFileId; fileId = newFileId;
} else if (record instanceof FitStressLevel) { } else if (record instanceof FitStressLevel) {
final Integer stress = ((FitStressLevel) record).getStressLevelValue(); final FitStressLevel stressRecord = (FitStressLevel) record;
if (stress == null || stress < 0) { final Integer stress = stressRecord.getStressLevelValue();
continue; 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) { } else if (record instanceof FitSleepStage) {
final FieldDefinitionSleepStage.SleepStage stage = ((FitSleepStage) record).getSleepStage(); final FieldDefinitionSleepStage.SleepStage stage = ((FitSleepStage) record).getSleepStage();
if (stage == null) { if (stage == null) {
@ -245,6 +257,7 @@ public class FitImporter {
persistActivitySamples(); persistActivitySamples();
persistSpo2Samples(); persistSpo2Samples();
persistStressSamples(); persistStressSamples();
persistBodyEnergySamples();
break; break;
case SLEEP: case SLEEP:
persistEvents(); persistEvents();
@ -397,9 +410,12 @@ public class FitImporter {
private void reset() { private void reset() {
activitySamplesPerTimestamp.clear(); activitySamplesPerTimestamp.clear();
stressSamples.clear(); stressSamples.clear();
bodyEnergySamples.clear();
spo2samples.clear(); spo2samples.clear();
events.clear(); events.clear();
sleepStageSamples.clear(); sleepStageSamples.clear();
hrvSummarySamples.clear();
hrvValueSamples.clear();
timesInZone.clear(); timesInZone.clear();
activityPoints.clear(); activityPoints.clear();
unknownRecords.clear(); unknownRecords.clear();
@ -655,4 +671,30 @@ public class FitImporter {
GB.toast(context, "Error saving stress samples", Toast.LENGTH_LONG, GB.ERROR, e); GB.toast(context, "Error saving stress samples", Toast.LENGTH_LONG, GB.ERROR, e);
} }
} }
private void persistBodyEnergySamples() {
if (bodyEnergySamples.isEmpty()) {
return;
}
LOG.debug("Will persist {} body energy samples", bodyEnergySamples.size());
try (DBHandler handler = GBApplication.acquireDB()) {
final DaoSession session = handler.getDaoSession();
final Device device = DBHelper.getDevice(gbDevice, session);
final User user = DBHelper.getUser(session);
final GarminBodyEnergySampleProvider sampleProvider = new GarminBodyEnergySampleProvider(gbDevice, session);
for (final GarminBodyEnergySample sample : bodyEnergySamples) {
sample.setDevice(device);
sample.setUser(user);
}
sampleProvider.addSamples(bodyEnergySamples);
} catch (final Exception e) {
GB.toast(context, "Error saving body energy samples", Toast.LENGTH_LONG, GB.ERROR, e);
}
}
} }

View File

@ -230,7 +230,8 @@ public class GlobalFITMessage {
public static GlobalFITMessage STRESS_LEVEL = new GlobalFITMessage(227, "STRESS_LEVEL", Arrays.asList( public static GlobalFITMessage STRESS_LEVEL = new GlobalFITMessage(227, "STRESS_LEVEL", Arrays.asList(
new FieldDefinitionPrimitive(0, BaseType.SINT16, "stress_level_value"), 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( public static GlobalFITMessage SPO2 = new GlobalFITMessage(269, "SPO2", Arrays.asList(

View File

@ -46,6 +46,11 @@ public class FitStressLevel extends RecordData {
return (Long) getFieldByNumber(1); return (Long) getFieldByNumber(1);
} }
@Nullable
public Integer getBodyEnergy() {
return (Integer) getFieldByNumber(3);
}
// manual changes below // manual changes below
@Override @Override