diff --git a/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java b/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java index 1f668551a..50c5f0057 100644 --- a/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java +++ b/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java @@ -63,6 +63,9 @@ public class GBDaoGenerator { addHuamiExtendedActivitySample(schema, user, device); addHuamiStressSample(schema, user, device); addHuamiSpo2Sample(schema, user, device); + addHuamiHeartRateManualSample(schema, user, device); + addHuamiHeartRateMaxSample(schema, user, device); + addHuamiHeartRateRestingSample(schema, user, device); addPebbleHealthActivitySample(schema, user, device); addPebbleHealthActivityKindOverlay(schema, user, device); addPebbleMisfitActivitySample(schema, user, device); @@ -257,6 +260,30 @@ public class GBDaoGenerator { return spo2sample; } + private static Entity addHuamiHeartRateManualSample(Schema schema, Entity user, Entity device) { + Entity hrManualSample = addEntity(schema, "HuamiHeartRateManualSample"); + addCommonTimeSampleProperties("AbstractHeartRateSample", hrManualSample, user, device); + hrManualSample.addIntProperty("utcOffset").notNull(); + hrManualSample.addIntProperty(SAMPLE_HEART_RATE).notNull().codeBeforeGetter(OVERRIDE); + return hrManualSample; + } + + private static Entity addHuamiHeartRateMaxSample(Schema schema, Entity user, Entity device) { + Entity hrMaxSample = addEntity(schema, "HuamiHeartRateMaxSample"); + addCommonTimeSampleProperties("AbstractHeartRateSample", hrMaxSample, user, device); + hrMaxSample.addIntProperty("utcOffset").notNull(); + hrMaxSample.addIntProperty(SAMPLE_HEART_RATE).notNull().codeBeforeGetter(OVERRIDE); + return hrMaxSample; + } + + private static Entity addHuamiHeartRateRestingSample(Schema schema, Entity user, Entity device) { + Entity hrRestingSample = addEntity(schema, "HuamiHeartRateRestingSample"); + addCommonTimeSampleProperties("AbstractHeartRateSample", hrRestingSample, user, device); + hrRestingSample.addIntProperty("utcOffset").notNull(); + hrRestingSample.addIntProperty(SAMPLE_HEART_RATE).notNull().codeBeforeGetter(OVERRIDE); + return hrRestingSample; + } + private static void addHeartRateProperties(Entity activitySample) { activitySample.addIntProperty(SAMPLE_HEART_RATE).notNull().codeBeforeGetterAndSetter(OVERRIDE); } 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 54c6907e2..ad15d3a56 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractDeviceCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractDeviceCoordinator.java @@ -56,6 +56,7 @@ import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate; import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser; import nodomain.freeyourgadget.gadgetbridge.model.BatteryConfig; +import nodomain.freeyourgadget.gadgetbridge.model.HeartRateSample; import nodomain.freeyourgadget.gadgetbridge.model.Spo2Sample; import nodomain.freeyourgadget.gadgetbridge.model.StressSample; import nodomain.freeyourgadget.gadgetbridge.util.Prefs; @@ -163,6 +164,21 @@ public abstract class AbstractDeviceCoordinator implements DeviceCoordinator { return null; } + @Override + public TimeSampleProvider getHeartRateMaxSampleProvider(GBDevice device, DaoSession session) { + return null; + } + + @Override + public TimeSampleProvider getHeartRateRestingSampleProvider(GBDevice device, DaoSession session) { + return null; + } + + @Override + public TimeSampleProvider getHeartRateManualSampleProvider(GBDevice device, DaoSession session) { + return null; + } + @Override @Nullable public ActivitySummaryParser getActivitySummaryParser(final GBDevice device) { @@ -245,6 +261,11 @@ public abstract class AbstractDeviceCoordinator implements DeviceCoordinator { return false; } + @Override + public boolean supportsHeartRateStats() { + return false; + } + @Override public boolean supportsAlarmSnoozing() { 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 25dd0fc9f..ffe928091 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceCoordinator.java @@ -41,6 +41,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample; 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.Spo2Sample; import nodomain.freeyourgadget.gadgetbridge.model.StressSample; @@ -211,6 +212,12 @@ public interface DeviceCoordinator { */ boolean supportsSpo2(); + /** + * Returns true if heart rate stats (max, resting, manual) measurement and fetching is supported + * by the device (with this coordinator). + */ + boolean supportsHeartRateStats(); + /** * Returns true if activity data fetching is supported AND possible at this * very moment. This will consider the device state (being connected/disconnected/busy...) @@ -238,6 +245,21 @@ public interface DeviceCoordinator { */ TimeSampleProvider getSpo2SampleProvider(GBDevice device, DaoSession session); + /** + * Returns the sample provider for max HR data, for the device being supported. + */ + TimeSampleProvider getHeartRateMaxSampleProvider(GBDevice device, DaoSession session); + + /** + * Returns the sample provider for resting HR data, for the device being supported. + */ + TimeSampleProvider getHeartRateRestingSampleProvider(GBDevice device, DaoSession session); + + /** + * Returns the sample provider for manual HR data, for the device being supported. + */ + TimeSampleProvider getHeartRateManualSampleProvider(GBDevice device, DaoSession session); + /** * Returns the {@link ActivitySummaryParser} for the device being supported. * diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/Huami2021Coordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/Huami2021Coordinator.java index d4b3a03e9..7f7f492e4 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/Huami2021Coordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/Huami2021Coordinator.java @@ -122,6 +122,11 @@ public abstract class Huami2021Coordinator extends HuamiCoordinator { return true; } + @Override + public boolean supportsHeartRateStats() { + return true; + } + @Override public boolean supportsMusicInfo() { return true; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/HuamiCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/HuamiCoordinator.java index 02299ce43..da8b95bfe 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/HuamiCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/HuamiCoordinator.java @@ -150,6 +150,21 @@ public abstract class HuamiCoordinator extends AbstractBLEDeviceCoordinator { return new HuamiSpo2SampleProvider(device, session); } + @Override + public HuamiHeartRateMaxSampleProvider getHeartRateMaxSampleProvider(final GBDevice device, final DaoSession session) { + return new HuamiHeartRateMaxSampleProvider(device, session); + } + + @Override + public HuamiHeartRateRestingSampleProvider getHeartRateRestingSampleProvider(final GBDevice device, final DaoSession session) { + return new HuamiHeartRateRestingSampleProvider(device, session); + } + + @Override + public HuamiHeartRateManualSampleProvider getHeartRateManualSampleProvider(final GBDevice device, final DaoSession session) { + return new HuamiHeartRateManualSampleProvider(device, session); + } + @Override public ActivitySummaryParser getActivitySummaryParser(final GBDevice device) { return new HuamiActivitySummaryParser(); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/HuamiHeartRateManualSampleProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/HuamiHeartRateManualSampleProvider.java new file mode 100644 index 000000000..4720a88ec --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/HuamiHeartRateManualSampleProvider.java @@ -0,0 +1,56 @@ +/* Copyright (C) 2023 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.huami; + +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.HuamiHeartRateManualSample; +import nodomain.freeyourgadget.gadgetbridge.entities.HuamiHeartRateManualSampleDao; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; + +public class HuamiHeartRateManualSampleProvider extends AbstractTimeSampleProvider { + public HuamiHeartRateManualSampleProvider(final GBDevice device, final DaoSession session) { + super(device, session); + } + + @NonNull + @Override + public AbstractDao getSampleDao() { + return getSession().getHuamiHeartRateManualSampleDao(); + } + + @NonNull + @Override + protected Property getTimestampSampleProperty() { + return HuamiHeartRateManualSampleDao.Properties.Timestamp; + } + + @NonNull + @Override + protected Property getDeviceIdentifierSampleProperty() { + return HuamiHeartRateManualSampleDao.Properties.DeviceId; + } + + @Override + public HuamiHeartRateManualSample createSample() { + return new HuamiHeartRateManualSample(); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/HuamiHeartRateMaxSampleProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/HuamiHeartRateMaxSampleProvider.java new file mode 100644 index 000000000..7d7dfc40b --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/HuamiHeartRateMaxSampleProvider.java @@ -0,0 +1,56 @@ +/* Copyright (C) 2023 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.huami; + +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.HuamiHeartRateMaxSample; +import nodomain.freeyourgadget.gadgetbridge.entities.HuamiHeartRateMaxSampleDao; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; + +public class HuamiHeartRateMaxSampleProvider extends AbstractTimeSampleProvider { + public HuamiHeartRateMaxSampleProvider(final GBDevice device, final DaoSession session) { + super(device, session); + } + + @NonNull + @Override + public AbstractDao getSampleDao() { + return getSession().getHuamiHeartRateMaxSampleDao(); + } + + @NonNull + @Override + protected Property getTimestampSampleProperty() { + return HuamiHeartRateMaxSampleDao.Properties.Timestamp; + } + + @NonNull + @Override + protected Property getDeviceIdentifierSampleProperty() { + return HuamiHeartRateMaxSampleDao.Properties.DeviceId; + } + + @Override + public HuamiHeartRateMaxSample createSample() { + return new HuamiHeartRateMaxSample(); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/HuamiHeartRateRestingSampleProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/HuamiHeartRateRestingSampleProvider.java new file mode 100644 index 000000000..4dd127a2b --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/HuamiHeartRateRestingSampleProvider.java @@ -0,0 +1,56 @@ +/* Copyright (C) 2023 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.huami; + +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.HuamiHeartRateRestingSample; +import nodomain.freeyourgadget.gadgetbridge.entities.HuamiHeartRateRestingSampleDao; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; + +public class HuamiHeartRateRestingSampleProvider extends AbstractTimeSampleProvider { + public HuamiHeartRateRestingSampleProvider(final GBDevice device, final DaoSession session) { + super(device, session); + } + + @NonNull + @Override + public AbstractDao getSampleDao() { + return getSession().getHuamiHeartRateRestingSampleDao(); + } + + @NonNull + @Override + protected Property getTimestampSampleProperty() { + return HuamiHeartRateRestingSampleDao.Properties.Timestamp; + } + + @NonNull + @Override + protected Property getDeviceIdentifierSampleProperty() { + return HuamiHeartRateRestingSampleDao.Properties.DeviceId; + } + + @Override + public HuamiHeartRateRestingSample createSample() { + return new HuamiHeartRateRestingSample(); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/entities/AbstractHeartRateSample.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/entities/AbstractHeartRateSample.java index f4472dbcb..b8108b41d 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/entities/AbstractHeartRateSample.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/entities/AbstractHeartRateSample.java @@ -18,7 +18,6 @@ package nodomain.freeyourgadget.gadgetbridge.entities; import androidx.annotation.NonNull; -import nodomain.freeyourgadget.gadgetbridge.devices.TimeSampleProvider; import nodomain.freeyourgadget.gadgetbridge.model.HeartRateSample; import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils; @@ -29,7 +28,6 @@ public abstract class AbstractHeartRateSample extends AbstractTimeSample impleme return getClass().getSimpleName() + "{" + "timestamp=" + DateTimeUtils.formatDateTime(DateTimeUtils.parseTimestampMillis(getTimestamp())) + ", hr=" + getHeartRate() + - ", type=" + getType() + ", userId=" + getUserId() + ", deviceId=" + getDeviceId() + "}"; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/HeartRateSample.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/HeartRateSample.java index 0860ab04f..e327118a2 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/HeartRateSample.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/HeartRateSample.java @@ -17,14 +17,6 @@ package nodomain.freeyourgadget.gadgetbridge.model; public interface HeartRateSample extends TimeSample { - int TYPE_MANUAL = 0; - int TYPE_AUTOMATIC_RESTING = 1; - - /** - * Returns the measurement type for this heart rate value. - */ - int getType(); - /** * Returns the heart rate value. */ diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/RecordedDataTypes.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/RecordedDataTypes.java index e4eed5137..d7006463c 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/RecordedDataTypes.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/RecordedDataTypes.java @@ -25,6 +25,7 @@ public class RecordedDataTypes { public static final int TYPE_DEBUGLOGS = 0x00000010; public static final int TYPE_SPO2 = 0x00000020; public static final int TYPE_STRESS = 0x00000040; + public static final int TYPE_HEART_RATE = 0x00000080; public static final int TYPE_ALL = (int)0xffffffff; } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/TimeSample.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/TimeSample.java index 2af23627a..9be675557 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/TimeSample.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/TimeSample.java @@ -16,8 +16,6 @@ along with this program. If not, see . */ package nodomain.freeyourgadget.gadgetbridge.model; -import nodomain.freeyourgadget.gadgetbridge.devices.TimeSampleProvider; - public interface TimeSample { /** * Unix timestamp of the sample, i.e. the number of milliseconds since 1970-01-01 00:00:00 UTC. diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiSupport.java index 44039f390..c6160700b 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiSupport.java @@ -118,6 +118,9 @@ import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser; import nodomain.freeyourgadget.gadgetbridge.model.Alarm; import nodomain.freeyourgadget.gadgetbridge.model.RecordedDataTypes; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.AbstractFetchOperation; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.FetchHeartRateManualOperation; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.FetchHeartRateMaxOperation; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.FetchHeartRateRestingOperation; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.FetchSpo2NormalOperation; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.FetchSportsSummaryOperation; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.FetchStressAutoOperation; @@ -1672,6 +1675,12 @@ public abstract class HuamiSupport extends AbstractBTLEDeviceSupport implements this.fetchOperationQueue.add(new FetchStressManualOperation(this)); } + if ((dataTypes & RecordedDataTypes.TYPE_HEART_RATE) != 0 && coordinator.supportsHeartRateStats()) { + this.fetchOperationQueue.add(new FetchHeartRateManualOperation(this)); + this.fetchOperationQueue.add(new FetchHeartRateMaxOperation(this)); + this.fetchOperationQueue.add(new FetchHeartRateRestingOperation(this)); + } + final AbstractFetchOperation nextOperation = this.fetchOperationQueue.poll(); if (nextOperation != null) { try { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/FetchHeartRateManualOperation.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/FetchHeartRateManualOperation.java new file mode 100644 index 000000000..e8ab2ef52 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/FetchHeartRateManualOperation.java @@ -0,0 +1,115 @@ +/* Copyright (C) 2023 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.service.devices.huami.operations; + +import android.widget.Toast; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.GregorianCalendar; +import java.util.List; + +import nodomain.freeyourgadget.gadgetbridge.GBApplication; +import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; +import nodomain.freeyourgadget.gadgetbridge.database.DBHelper; +import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiCoordinator; +import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiHeartRateManualSampleProvider; +import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiService; +import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; +import nodomain.freeyourgadget.gadgetbridge.entities.Device; +import nodomain.freeyourgadget.gadgetbridge.entities.HuamiHeartRateManualSample; +import nodomain.freeyourgadget.gadgetbridge.entities.HuamiHeartRateManualSample; +import nodomain.freeyourgadget.gadgetbridge.entities.User; +import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiSupport; +import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper; +import nodomain.freeyourgadget.gadgetbridge.util.GB; + +/** + * An operation that fetches manual HR measurement data. + */ +public class FetchHeartRateManualOperation extends AbstractRepeatingFetchOperation { + private static final Logger LOG = LoggerFactory.getLogger(FetchHeartRateManualOperation.class); + + public FetchHeartRateManualOperation(final HuamiSupport support) { + super(support, HuamiService.COMMAND_ACTIVITY_DATA_TYPE_MANUAL_HEART_RATE, "manual hr data"); + } + + @Override + protected boolean handleActivityData(final GregorianCalendar timestamp, final byte[] bytes) { + if (bytes.length % 6 != 0) { + LOG.warn("Unexpected buffered manual heart rate data size {} is not a multiple of 6", bytes.length); + return false; + } + + final List samples = new ArrayList<>(); + + final ByteBuffer buffer = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN); + + while (buffer.position() < bytes.length) { + final long currentTimestamp = BLETypeConversions.toUnsigned(buffer.getInt()) * 1000; + timestamp.setTimeInMillis(currentTimestamp); + + final byte utcOffsetInQuarterHours = buffer.get(); + final int hr = buffer.get() & 0xff; + + LOG.trace("Manual HR at {} + {}: {}", timestamp.getTime(), utcOffsetInQuarterHours, hr); + + final HuamiHeartRateManualSample sample = new HuamiHeartRateManualSample(); + sample.setTimestamp(timestamp.getTimeInMillis()); + sample.setHeartRate(hr); + sample.setUtcOffset(utcOffsetInQuarterHours * 900000); + samples.add(sample); + } + + return persistSamples(samples); + } + + protected boolean persistSamples(final List samples) { + try (DBHandler handler = GBApplication.acquireDB()) { + final DaoSession session = handler.getDaoSession(); + + final Device device = DBHelper.getDevice(getDevice(), session); + final User user = DBHelper.getUser(session); + + final HuamiCoordinator coordinator = (HuamiCoordinator) DeviceHelper.getInstance().getCoordinator(getDevice()); + final HuamiHeartRateManualSampleProvider sampleProvider = coordinator.getHeartRateManualSampleProvider(getDevice(), session); + + for (final HuamiHeartRateManualSample sample : samples) { + sample.setDevice(device); + sample.setUser(user); + } + + LOG.debug("Will persist {} heart rate manual samples", samples.size()); + sampleProvider.addSamples(samples); + } catch (final Exception e) { + GB.toast(getContext(), "Error saving heart rate manual samples", Toast.LENGTH_LONG, GB.ERROR, e); + return false; + } + + return true; + } + + @Override + protected String getLastSyncTimeKey() { + return "lastHeartRateManualTimeMillis"; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/FetchHeartRateMaxOperation.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/FetchHeartRateMaxOperation.java new file mode 100644 index 000000000..6869f4b7b --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/FetchHeartRateMaxOperation.java @@ -0,0 +1,114 @@ +/* Copyright (C) 2023 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.service.devices.huami.operations; + +import android.widget.Toast; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.GregorianCalendar; +import java.util.List; + +import nodomain.freeyourgadget.gadgetbridge.GBApplication; +import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; +import nodomain.freeyourgadget.gadgetbridge.database.DBHelper; +import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiCoordinator; +import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiService; +import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiHeartRateMaxSampleProvider; +import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; +import nodomain.freeyourgadget.gadgetbridge.entities.Device; +import nodomain.freeyourgadget.gadgetbridge.entities.HuamiHeartRateMaxSample; +import nodomain.freeyourgadget.gadgetbridge.entities.User; +import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiSupport; +import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper; +import nodomain.freeyourgadget.gadgetbridge.util.GB; + +/** + * An operation that fetches max HR data. + */ +public class FetchHeartRateMaxOperation extends AbstractRepeatingFetchOperation { + private static final Logger LOG = LoggerFactory.getLogger(FetchHeartRateMaxOperation.class); + + public FetchHeartRateMaxOperation(final HuamiSupport support) { + super(support, HuamiService.COMMAND_ACTIVITY_DATA_TYPE_MAX_HEART_RATE, "max hr data"); + } + + @Override + protected boolean handleActivityData(final GregorianCalendar timestamp, final byte[] bytes) { + if (bytes.length % 6 != 0) { + LOG.warn("Unexpected buffered max heart rate data size {} is not a multiple of 6", bytes.length); + return false; + } + + final List samples = new ArrayList<>(); + + final ByteBuffer buffer = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN); + + while (buffer.position() < bytes.length) { + final long currentTimestamp = BLETypeConversions.toUnsigned(buffer.getInt()) * 1000; + timestamp.setTimeInMillis(currentTimestamp); + + final byte utcOffsetInQuarterHours = buffer.get(); + final int hr = buffer.get() & 0xff; + + LOG.trace("Max HR at {} + {}: {}", timestamp.getTime(), utcOffsetInQuarterHours, hr); + + final HuamiHeartRateMaxSample sample = new HuamiHeartRateMaxSample(); + sample.setTimestamp(timestamp.getTimeInMillis()); + sample.setHeartRate(hr); + sample.setUtcOffset(utcOffsetInQuarterHours * 900000); + samples.add(sample); + } + + return persistSamples(samples); + } + + protected boolean persistSamples(final List samples) { + try (DBHandler handler = GBApplication.acquireDB()) { + final DaoSession session = handler.getDaoSession(); + + final Device device = DBHelper.getDevice(getDevice(), session); + final User user = DBHelper.getUser(session); + + final HuamiCoordinator coordinator = (HuamiCoordinator) DeviceHelper.getInstance().getCoordinator(getDevice()); + final HuamiHeartRateMaxSampleProvider sampleProvider = coordinator.getHeartRateMaxSampleProvider(getDevice(), session); + + for (final HuamiHeartRateMaxSample sample : samples) { + sample.setDevice(device); + sample.setUser(user); + } + + LOG.debug("Will persist {} heart rate max samples", samples.size()); + sampleProvider.addSamples(samples); + } catch (final Exception e) { + GB.toast(getContext(), "Error saving heart rate max samples", Toast.LENGTH_LONG, GB.ERROR, e); + return false; + } + + return true; + } + + @Override + protected String getLastSyncTimeKey() { + return "lastHeartRateMaxTimeMillis"; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/FetchHeartRateRestingOperation.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/FetchHeartRateRestingOperation.java new file mode 100644 index 000000000..155514fe3 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/FetchHeartRateRestingOperation.java @@ -0,0 +1,115 @@ +/* Copyright (C) 2023 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.service.devices.huami.operations; + +import android.widget.Toast; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.GregorianCalendar; +import java.util.List; + +import nodomain.freeyourgadget.gadgetbridge.GBApplication; +import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; +import nodomain.freeyourgadget.gadgetbridge.database.DBHelper; +import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiCoordinator; +import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiHeartRateRestingSampleProvider; +import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiService; +import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; +import nodomain.freeyourgadget.gadgetbridge.entities.Device; +import nodomain.freeyourgadget.gadgetbridge.entities.HuamiHeartRateRestingSample; +import nodomain.freeyourgadget.gadgetbridge.entities.User; +import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiSupport; +import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper; +import nodomain.freeyourgadget.gadgetbridge.util.GB; + +/** + * An operation that fetches resting heart rate data. + */ +public class FetchHeartRateRestingOperation extends AbstractRepeatingFetchOperation { + private static final Logger LOG = LoggerFactory.getLogger(FetchHeartRateRestingOperation.class); + + public FetchHeartRateRestingOperation(final HuamiSupport support) { + super(support, HuamiService.COMMAND_ACTIVITY_DATA_TYPE_RESTING_HEART_RATE, "resting hr data"); + } + + @Override + protected boolean handleActivityData(final GregorianCalendar timestamp, final byte[] bytes) { + if (bytes.length % 6 != 0) { + LOG.warn("Unexpected buffered rest heart rate data size {} is not a multiple of 6", bytes.length); + return false; + } + + final List samples = new ArrayList<>(); + + final ByteBuffer buffer = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN); + + while (buffer.position() < bytes.length) { + final long currentTimestamp = BLETypeConversions.toUnsigned(buffer.getInt()) * 1000; + timestamp.setTimeInMillis(currentTimestamp); + + // Official app only shows this at day-level + final byte utcOffsetInQuarterHours = buffer.get(); + final int hr = buffer.get() & 0xff; + + LOG.trace("Resting HR at {} + {}: {}", timestamp.getTime(), utcOffsetInQuarterHours, hr); + + final HuamiHeartRateRestingSample sample = new HuamiHeartRateRestingSample(); + sample.setTimestamp(timestamp.getTimeInMillis()); + sample.setHeartRate(hr); + sample.setUtcOffset(utcOffsetInQuarterHours * 900000); + samples.add(sample); + } + + return persistSamples(samples); + } + + protected boolean persistSamples(final List samples) { + try (DBHandler handler = GBApplication.acquireDB()) { + final DaoSession session = handler.getDaoSession(); + + final Device device = DBHelper.getDevice(getDevice(), session); + final User user = DBHelper.getUser(session); + + final HuamiCoordinator coordinator = (HuamiCoordinator) DeviceHelper.getInstance().getCoordinator(getDevice()); + final HuamiHeartRateRestingSampleProvider sampleProvider = coordinator.getHeartRateRestingSampleProvider(getDevice(), session); + + for (final HuamiHeartRateRestingSample sample : samples) { + sample.setDevice(device); + sample.setUser(user); + } + + LOG.debug("Will persist {} heart rate resting samples", samples.size()); + sampleProvider.addSamples(samples); + } catch (final Exception e) { + GB.toast(getContext(), "Error saving heart rate resting samples", Toast.LENGTH_LONG, GB.ERROR, e); + return false; + } + + return true; + } + + @Override + protected String getLastSyncTimeKey() { + return "lastHeartRateRestingTimeMillis"; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/FetchManualHeartRateOperation.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/FetchManualHeartRateOperation.java deleted file mode 100644 index 45858d1d2..000000000 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/FetchManualHeartRateOperation.java +++ /dev/null @@ -1,68 +0,0 @@ -/* Copyright (C) 2023 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.service.devices.huami.operations; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.nio.ByteBuffer; -import java.nio.ByteOrder; -import java.util.GregorianCalendar; - -import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiService; -import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions; -import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiSupport; - -/** - * An operation that fetches manual HR measurement data. - */ -public class FetchManualHeartRateOperation extends AbstractRepeatingFetchOperation { - private static final Logger LOG = LoggerFactory.getLogger(FetchManualHeartRateOperation.class); - - public FetchManualHeartRateOperation(final HuamiSupport support) { - super(support, HuamiService.COMMAND_ACTIVITY_DATA_TYPE_MANUAL_HEART_RATE, "manual hr data"); - } - - @Override - protected boolean handleActivityData(final GregorianCalendar timestamp, final byte[] bytes) { - if (bytes.length % 6 != 0) { - LOG.warn("Unexpected buffered manual heart rate data size {} is not a multiple of 6", bytes.length); - return false; - } - - final ByteBuffer buffer = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN); - - while (buffer.position() < bytes.length) { - final long currentTimestamp = BLETypeConversions.toUnsigned(buffer.getInt()) * 1000; - timestamp.setTimeInMillis(currentTimestamp); - - final byte utcOffsetInQuarterHours = buffer.get(); - final int hr = buffer.get() & 0xff; - - LOG.info("Manual HR at {} + {}: {}", timestamp.getTime(), utcOffsetInQuarterHours, hr); - - // TODO: Save manual hr data - } - - return true; - } - - @Override - protected String getLastSyncTimeKey() { - return "lastManualHeartRateTimeMillis"; - } -} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/FetchMaxHeartRateOperation.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/FetchMaxHeartRateOperation.java deleted file mode 100644 index 0f255de1d..000000000 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/FetchMaxHeartRateOperation.java +++ /dev/null @@ -1,68 +0,0 @@ -/* Copyright (C) 2023 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.service.devices.huami.operations; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.nio.ByteBuffer; -import java.nio.ByteOrder; -import java.util.GregorianCalendar; - -import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiService; -import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions; -import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiSupport; - -/** - * An operation that fetches max HR data. - */ -public class FetchMaxHeartRateOperation extends AbstractRepeatingFetchOperation { - private static final Logger LOG = LoggerFactory.getLogger(FetchMaxHeartRateOperation.class); - - public FetchMaxHeartRateOperation(final HuamiSupport support) { - super(support, HuamiService.COMMAND_ACTIVITY_DATA_TYPE_MAX_HEART_RATE, "max hr data"); - } - - @Override - protected boolean handleActivityData(final GregorianCalendar timestamp, final byte[] bytes) { - if (bytes.length % 6 != 0) { - LOG.warn("Unexpected buffered max heart rate data size {} is not a multiple of 6", bytes.length); - return false; - } - - final ByteBuffer buffer = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN); - - while (buffer.position() < bytes.length) { - final long currentTimestamp = BLETypeConversions.toUnsigned(buffer.getInt()) * 1000; - timestamp.setTimeInMillis(currentTimestamp); - - final byte utcOffsetInQuarterHours = buffer.get(); - final int hr = buffer.get() & 0xff; - - LOG.debug("Max HR at {} + {}: {}", timestamp.getTime(), utcOffsetInQuarterHours, hr); - - // TODO: Save max hr data - } - - return true; - } - - @Override - protected String getLastSyncTimeKey() { - return "lastMaxHeartRateTimeMillis"; - } -} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/FetchRestingHeartRateOperation.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/FetchRestingHeartRateOperation.java deleted file mode 100644 index 6ecb1c676..000000000 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/FetchRestingHeartRateOperation.java +++ /dev/null @@ -1,69 +0,0 @@ -/* Copyright (C) 2023 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.service.devices.huami.operations; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.nio.ByteBuffer; -import java.nio.ByteOrder; -import java.util.GregorianCalendar; - -import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiService; -import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions; -import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiSupport; - -/** - * An operation that fetches resting heart rate data. - */ -public class FetchRestingHeartRateOperation extends AbstractRepeatingFetchOperation { - private static final Logger LOG = LoggerFactory.getLogger(FetchRestingHeartRateOperation.class); - - public FetchRestingHeartRateOperation(final HuamiSupport support) { - super(support, HuamiService.COMMAND_ACTIVITY_DATA_TYPE_RESTING_HEART_RATE, "resting hr data"); - } - - @Override - protected boolean handleActivityData(final GregorianCalendar timestamp, final byte[] bytes) { - if (bytes.length % 6 != 0) { - LOG.warn("Unexpected buffered rest heart rate data size {} is not a multiple of 6", bytes.length); - return false; - } - - final ByteBuffer buffer = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN); - - while (buffer.position() < bytes.length) { - final long currentTimestamp = BLETypeConversions.toUnsigned(buffer.getInt()) * 1000; - timestamp.setTimeInMillis(currentTimestamp); - - // Official app only shows this at day-level - final byte utcOffsetInQuarterHours = buffer.get(); - final int hr = buffer.get() & 0xff; - - LOG.debug("Resting HR at {} + {}: {}", timestamp.getTime(), utcOffsetInQuarterHours, hr); - - // TODO: Save resting hr data - } - - return true; - } - - @Override - protected String getLastSyncTimeKey() { - return "lastRestingHeartRateTimeMillis"; - } -}