diff --git a/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java b/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java index 02a86ce21..d4c6ea089 100644 --- a/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java +++ b/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java @@ -67,6 +67,7 @@ public class GBDaoGenerator { addHuamiHeartRateMaxSample(schema, user, device); addHuamiHeartRateRestingSample(schema, user, device); addHuamiPaiSample(schema, user, device); + addHuamiSleepRespiratoryRateSample(schema, user, device); addPebbleHealthActivitySample(schema, user, device); addPebbleHealthActivityKindOverlay(schema, user, device); addPebbleMisfitActivitySample(schema, user, device); @@ -300,6 +301,14 @@ public class GBDaoGenerator { return paiSample; } + private static Entity addHuamiSleepRespiratoryRateSample(Schema schema, Entity user, Entity device) { + Entity sleepRespiratoryRateSample = addEntity(schema, "HuamiSleepRespiratoryRateSample"); + addCommonTimeSampleProperties("AbstractSleepRespiratoryRateSample", sleepRespiratoryRateSample, user, device); + sleepRespiratoryRateSample.addIntProperty("utcOffset").notNull(); + sleepRespiratoryRateSample.addIntProperty("rate").notNull().codeBeforeGetter(OVERRIDE); + return sleepRespiratoryRateSample; + } + 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 6c2071e02..427b85d7e 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractDeviceCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractDeviceCoordinator.java @@ -58,6 +58,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser; import nodomain.freeyourgadget.gadgetbridge.model.BatteryConfig; import nodomain.freeyourgadget.gadgetbridge.model.HeartRateSample; import nodomain.freeyourgadget.gadgetbridge.model.PaiSample; +import nodomain.freeyourgadget.gadgetbridge.model.SleepRespiratoryRateSample; import nodomain.freeyourgadget.gadgetbridge.model.Spo2Sample; import nodomain.freeyourgadget.gadgetbridge.model.StressSample; import nodomain.freeyourgadget.gadgetbridge.util.Prefs; @@ -185,6 +186,11 @@ public abstract class AbstractDeviceCoordinator implements DeviceCoordinator { return null; } + @Override + public TimeSampleProvider getSleepRespiratoryRateSampleProvider(GBDevice device, DaoSession session) { + return null; + } + @Override @Nullable public ActivitySummaryParser getActivitySummaryParser(final GBDevice device) { @@ -277,6 +283,11 @@ public abstract class AbstractDeviceCoordinator implements DeviceCoordinator { return false; } + @Override + public boolean supportsSleepRespiratoryRate() { + 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 060daf500..84aab0db4 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceCoordinator.java @@ -43,6 +43,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.BatteryConfig; import nodomain.freeyourgadget.gadgetbridge.model.DeviceType; import nodomain.freeyourgadget.gadgetbridge.model.HeartRateSample; import nodomain.freeyourgadget.gadgetbridge.model.PaiSample; +import nodomain.freeyourgadget.gadgetbridge.model.SleepRespiratoryRateSample; import nodomain.freeyourgadget.gadgetbridge.model.Spo2Sample; import nodomain.freeyourgadget.gadgetbridge.model.StressSample; @@ -225,6 +226,12 @@ public interface DeviceCoordinator { */ boolean supportsPai(); + /** + * Returns true if sleep respiratory rate measurement and fetching is supported by + * the device (with this coordinator). + */ + boolean supportsSleepRespiratoryRate(); + /** * Returns true if activity data fetching is supported AND possible at this * very moment. This will consider the device state (being connected/disconnected/busy...) @@ -272,6 +279,11 @@ public interface DeviceCoordinator { */ TimeSampleProvider getPaiSampleProvider(GBDevice device, DaoSession session); + /** + * Returns the sample provider for sleep respiratory rate data, for the device being supported. + */ + TimeSampleProvider getSleepRespiratoryRateSampleProvider(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 6148beb30..a78a406ff 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 @@ -132,6 +132,11 @@ public abstract class Huami2021Coordinator extends HuamiCoordinator { return true; } + @Override + public boolean supportsSleepRespiratoryRate() { + 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 2b7f6a199..8d12104ea 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 @@ -170,6 +170,11 @@ public abstract class HuamiCoordinator extends AbstractBLEDeviceCoordinator { return new HuamiPaiSampleProvider(device, session); } + @Override + public HuamiSleepRespiratoryRateSampleProvider getSleepRespiratoryRateSampleProvider(GBDevice device, DaoSession session) { + return new HuamiSleepRespiratoryRateSampleProvider(device, session); + } + @Override public ActivitySummaryParser getActivitySummaryParser(final GBDevice device) { return new HuamiActivitySummaryParser(); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/HuamiSleepRespiratoryRateSampleProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/HuamiSleepRespiratoryRateSampleProvider.java new file mode 100644 index 000000000..213aa3eae --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/HuamiSleepRespiratoryRateSampleProvider.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.HuamiSleepRespiratoryRateSample; +import nodomain.freeyourgadget.gadgetbridge.entities.HuamiSleepRespiratoryRateSampleDao; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; + +public class HuamiSleepRespiratoryRateSampleProvider extends AbstractTimeSampleProvider { + public HuamiSleepRespiratoryRateSampleProvider(final GBDevice device, final DaoSession session) { + super(device, session); + } + + @NonNull + @Override + public AbstractDao getSampleDao() { + return getSession().getHuamiSleepRespiratoryRateSampleDao(); + } + + @NonNull + @Override + protected Property getTimestampSampleProperty() { + return HuamiSleepRespiratoryRateSampleDao.Properties.Timestamp; + } + + @NonNull + @Override + protected Property getDeviceIdentifierSampleProperty() { + return HuamiSleepRespiratoryRateSampleDao.Properties.DeviceId; + } + + @Override + public HuamiSleepRespiratoryRateSample createSample() { + return new HuamiSleepRespiratoryRateSample(); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/entities/AbstractSleepRespiratoryRateSample.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/entities/AbstractSleepRespiratoryRateSample.java new file mode 100644 index 000000000..2d35a63f1 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/entities/AbstractSleepRespiratoryRateSample.java @@ -0,0 +1,35 @@ +/* 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.entities; + +import androidx.annotation.NonNull; + +import nodomain.freeyourgadget.gadgetbridge.model.SleepRespiratoryRateSample; +import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils; + +public abstract class AbstractSleepRespiratoryRateSample extends AbstractTimeSample implements SleepRespiratoryRateSample { + @NonNull + @Override + public String toString() { + return getClass().getSimpleName() + "{" + + "timestamp=" + DateTimeUtils.formatDateTime(DateTimeUtils.parseTimestampMillis(getTimestamp())) + + ", rate=" + getRate() + + ", userId=" + getUserId() + + ", deviceId=" + getDeviceId() + + "}"; + } +} 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 e3bc2ef12..0c5586e38 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/RecordedDataTypes.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/RecordedDataTypes.java @@ -27,6 +27,7 @@ public class RecordedDataTypes { public static final int TYPE_STRESS = 0x00000040; public static final int TYPE_HEART_RATE = 0x00000080; public static final int TYPE_PAI = 0x00000100; + public static final int TYPE_SLEEP_RESPIRATORY_RATE = 0x00000200; public static final int TYPE_ALL = (int)0xffffffff; } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/SleepRespiratoryRateSample.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/SleepRespiratoryRateSample.java new file mode 100644 index 000000000..7c3a0f3cc --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/SleepRespiratoryRateSample.java @@ -0,0 +1,24 @@ +/* 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.model; + +public interface SleepRespiratoryRateSample extends TimeSample { + /** + * Returns the respiratory rate value, in breaths per minute. + */ + int getRate(); +} 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 2261b9509..aa8b4a5b3 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 @@ -122,6 +122,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.Fet 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.FetchPaiOperation; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.FetchSleepRespiratoryRateOperation; 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; @@ -1686,6 +1687,10 @@ public abstract class HuamiSupport extends AbstractBTLEDeviceSupport implements this.fetchOperationQueue.add(new FetchPaiOperation(this)); } + if ((dataTypes & RecordedDataTypes.TYPE_SLEEP_RESPIRATORY_RATE) != 0 && coordinator.supportsSleepRespiratoryRate()) { + this.fetchOperationQueue.add(new FetchSleepRespiratoryRateOperation(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/FetchSleepRespiratoryRateOperation.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/FetchSleepRespiratoryRateOperation.java index 5b86eb16f..d71c21508 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/FetchSleepRespiratoryRateOperation.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/FetchSleepRespiratoryRateOperation.java @@ -16,15 +16,30 @@ 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.HuamiSleepRespiratoryRateSampleProvider; +import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; +import nodomain.freeyourgadget.gadgetbridge.entities.Device; +import nodomain.freeyourgadget.gadgetbridge.entities.HuamiSleepRespiratoryRateSample; +import nodomain.freeyourgadget.gadgetbridge.entities.User; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiSupport; +import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper; +import nodomain.freeyourgadget.gadgetbridge.util.GB; /** * An operation that fetches sleep respiratory rate data. @@ -43,6 +58,8 @@ public class FetchSleepRespiratoryRateOperation extends AbstractRepeatingFetchOp return false; } + final List samples = new ArrayList<>(); + final ByteBuffer buf = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN); while (buf.position() < bytes.length) { @@ -54,8 +71,37 @@ public class FetchSleepRespiratoryRateOperation extends AbstractRepeatingFetchOp timestamp.setTimeInMillis(timestampSeconds * 1000L); - LOG.debug("Sleep Respiratory Rate at {} + {}: respiratoryRate={} unknown1={} unknown2={}", timestamp.getTime(), respiratoryRate, utcOffsetInQuarterHours, unknown1, unknown2); - // TODO save + LOG.trace("Sleep Respiratory Rate at {} + {}: respiratoryRate={} unknown1={} unknown2={}", timestamp.getTime(), respiratoryRate, utcOffsetInQuarterHours, unknown1, unknown2); + final HuamiSleepRespiratoryRateSample sample = new HuamiSleepRespiratoryRateSample(); + sample.setTimestamp(timestamp.getTimeInMillis()); + sample.setUtcOffset(utcOffsetInQuarterHours * 900000); + sample.setRate(respiratoryRate); + 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 HuamiSleepRespiratoryRateSampleProvider sampleProvider = coordinator.getSleepRespiratoryRateSampleProvider(getDevice(), session); + + for (final HuamiSleepRespiratoryRateSample sample : samples) { + sample.setDevice(device); + sample.setUser(user); + } + + LOG.debug("Will persist {} sleep respiratory rate samples", samples.size()); + sampleProvider.addSamples(samples); + } catch (final Exception e) { + GB.toast(getContext(), "Error saving sleep respiratory rate samples", Toast.LENGTH_LONG, GB.ERROR, e); + return false; } return true;