From 23e9a3deb114acfb7525f927375f2d09ba9ffe33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Rebelo?= Date: Sat, 20 May 2023 23:34:10 +0100 Subject: [PATCH] Huami: Persist stress and SpO2 data --- .../gadgetbridge/daogen/GBDaoGenerator.java | 33 ++++++++++- .../devices/AbstractDeviceCoordinator.java | 22 +++++++ .../devices/DeviceCoordinator.java | 26 +++++++- .../devices/huami/Huami2021Coordinator.java | 10 ++++ .../devices/huami/HuamiCoordinator.java | 10 ++++ .../huami/HuamiSpo2SampleProvider.java | 56 ++++++++++++++++++ .../huami/HuamiStressSampleProvider.java | 56 ++++++++++++++++++ .../amazfitband5/AmazfitBand5Coordinator.java | 5 ++ .../huami/miband5/MiBand5Coordinator.java | 5 ++ .../entities/AbstractHeartRateSample.java | 37 ++++++++++++ .../entities/AbstractSpo2Sample.java | 48 +++++++++++++++ .../entities/AbstractStressSample.java | 48 +++++++++++++++ .../gadgetbridge/model/HeartRateSample.java | 32 ++++++++++ .../gadgetbridge/model/Spo2Sample.java | 55 +++++++++++++++++ .../gadgetbridge/model/StressSample.java | 59 +++++++++++++++++++ .../AbstractRepeatingFetchOperation.java | 8 +-- .../operations/FetchSpo2NormalOperation.java | 53 ++++++++++++++++- .../operations/FetchSpo2SleepOperation.java | 2 +- .../operations/FetchStressAutoOperation.java | 54 +++++++++++++++-- .../FetchStressManualOperation.java | 51 +++++++++++++++- 20 files changed, 654 insertions(+), 16 deletions(-) create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/HuamiSpo2SampleProvider.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/HuamiStressSampleProvider.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/entities/AbstractHeartRateSample.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/entities/AbstractSpo2Sample.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/entities/AbstractStressSample.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/HeartRateSample.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/Spo2Sample.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/StressSample.java diff --git a/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java b/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java index 633b800d7..1f668551a 100644 --- a/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java +++ b/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java @@ -43,7 +43,7 @@ public class GBDaoGenerator { public static void main(String[] args) throws Exception { - final Schema schema = new Schema(46, MAIN_PACKAGE + ".entities"); + final Schema schema = new Schema(47, MAIN_PACKAGE + ".entities"); Entity userAttributes = addUserAttributes(schema); Entity user = addUserInfo(schema, userAttributes); @@ -61,6 +61,8 @@ public class GBDaoGenerator { addMakibesHR3ActivitySample(schema, user, device); addMiBandActivitySample(schema, user, device); addHuamiExtendedActivitySample(schema, user, device); + addHuamiStressSample(schema, user, device); + addHuamiSpo2Sample(schema, user, device); addPebbleHealthActivitySample(schema, user, device); addPebbleHealthActivityKindOverlay(schema, user, device); addPebbleMisfitActivitySample(schema, user, device); @@ -239,6 +241,22 @@ public class GBDaoGenerator { return activitySample; } + private static Entity addHuamiStressSample(Schema schema, Entity user, Entity device) { + Entity stressSample = addEntity(schema, "HuamiStressSample"); + addCommonTimeSampleProperties("AbstractStressSample", stressSample, user, device); + stressSample.addIntProperty("typeNum").notNull().codeBeforeGetterAndSetter(OVERRIDE); + stressSample.addIntProperty("stress").notNull().codeBeforeGetter(OVERRIDE); + return stressSample; + } + + private static Entity addHuamiSpo2Sample(Schema schema, Entity user, Entity device) { + Entity spo2sample = addEntity(schema, "HuamiSpo2Sample"); + addCommonTimeSampleProperties("AbstractSpo2Sample", spo2sample, user, device); + spo2sample.addIntProperty("typeNum").notNull().codeBeforeGetterAndSetter(OVERRIDE); + spo2sample.addIntProperty("spo2").notNull().codeBeforeGetter(OVERRIDE); + return spo2sample; + } + private static void addHeartRateProperties(Entity activitySample) { activitySample.addIntProperty(SAMPLE_HEART_RATE).notNull().codeBeforeGetterAndSetter(OVERRIDE); } @@ -518,6 +536,19 @@ public class GBDaoGenerator { activitySample.addToOne(user, userId); } + private static void addCommonTimeSampleProperties(String superClass, Entity timeSample, Entity user, Entity device) { + timeSample.setSuperclass(superClass); + timeSample.addImport(MAIN_PACKAGE + ".devices.TimeSampleProvider"); + timeSample.setJavaDoc( + "This class represents a sample specific to the device. Values might be device specific, depending on the sample type.\n" + + "Normalized values can be retrieved through the corresponding {@link TimeSampleProvider}."); + timeSample.addLongProperty("timestamp").notNull().codeBeforeGetterAndSetter(OVERRIDE).primaryKey(); + Property deviceId = timeSample.addLongProperty("deviceId").primaryKey().notNull().codeBeforeGetterAndSetter(OVERRIDE).getProperty(); + timeSample.addToOne(device, deviceId); + Property userId = timeSample.addLongProperty("userId").notNull().codeBeforeGetterAndSetter(OVERRIDE).getProperty(); + timeSample.addToOne(user, userId); + } + private static void addCalendarSyncState(Schema schema, Entity device) { Entity calendarSyncState = addEntity(schema, "CalendarSyncState"); calendarSyncState.addIdProperty(); 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 65c044cf5..54c6907e2 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,8 @@ 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.Spo2Sample; +import nodomain.freeyourgadget.gadgetbridge.model.StressSample; import nodomain.freeyourgadget.gadgetbridge.util.Prefs; import static nodomain.freeyourgadget.gadgetbridge.GBApplication.getPrefs; @@ -151,6 +153,16 @@ public abstract class AbstractDeviceCoordinator implements DeviceCoordinator { return device.isInitialized() && !device.isBusy() && supportsActivityDataFetching(); } + @Override + public TimeSampleProvider getStressSampleProvider(GBDevice device, DaoSession session) { + return null; + } + + @Override + public TimeSampleProvider getSpo2SampleProvider(GBDevice device, DaoSession session) { + return null; + } + @Override @Nullable public ActivitySummaryParser getActivitySummaryParser(final GBDevice device) { @@ -223,6 +235,16 @@ public abstract class AbstractDeviceCoordinator implements DeviceCoordinator { return false; } + @Override + public boolean supportsStressMeasurement() { + return false; + } + + @Override + public boolean supportsSpo2() { + 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 39ef9ab3f..25dd0fc9f 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,8 @@ 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.Spo2Sample; +import nodomain.freeyourgadget.gadgetbridge.model.StressSample; /** * This interface is implemented at least once for every supported gadget device. @@ -183,7 +185,7 @@ public interface DeviceCoordinator { /** * Returns true if activity tracking is supported by the device * (with this coordinator). - * This enables the ChartsActivity. + * This enables the ActivityChartsActivity. * * @return */ @@ -197,6 +199,18 @@ public interface DeviceCoordinator { */ boolean supportsActivityTracks(); + /** + * Returns true if stress measurement and fetching is supported by the device + * (with this coordinator). + */ + boolean supportsStressMeasurement(); + + /** + * Returns true if SpO2 measurement and fetching is supported by the device + * (with this coordinator). + */ + boolean supportsSpo2(); + /** * Returns true if activity data fetching is supported AND possible at this * very moment. This will consider the device state (being connected/disconnected/busy...) @@ -214,6 +228,16 @@ public interface DeviceCoordinator { */ SampleProvider getSampleProvider(GBDevice device, DaoSession session); + /** + * Returns the sample provider for stress data, for the device being supported. + */ + TimeSampleProvider getStressSampleProvider(GBDevice device, DaoSession session); + + /** + * Returns the sample provider for SpO2 data, for the device being supported. + */ + TimeSampleProvider getSpo2SampleProvider(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 fbc1a47b5..efd42206a 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 @@ -112,6 +112,16 @@ public abstract class Huami2021Coordinator extends HuamiCoordinator { return true; } + @Override + public boolean supportsStressMeasurement() { + return true; + } + + @Override + public boolean supportsSpo2() { + 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 7935085f3..02299ce43 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 @@ -140,6 +140,16 @@ public abstract class HuamiCoordinator extends AbstractBLEDeviceCoordinator { return new MiBand2SampleProvider(device, session); } + @Override + public HuamiStressSampleProvider getStressSampleProvider(final GBDevice device, final DaoSession session) { + return new HuamiStressSampleProvider(device, session); + } + + @Override + public HuamiSpo2SampleProvider getSpo2SampleProvider(final GBDevice device, final DaoSession session) { + return new HuamiSpo2SampleProvider(device, session); + } + @Override public ActivitySummaryParser getActivitySummaryParser(final GBDevice device) { return new HuamiActivitySummaryParser(); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/HuamiSpo2SampleProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/HuamiSpo2SampleProvider.java new file mode 100644 index 000000000..a23e89401 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/HuamiSpo2SampleProvider.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.HuamiSpo2Sample; +import nodomain.freeyourgadget.gadgetbridge.entities.HuamiSpo2SampleDao; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; + +public class HuamiSpo2SampleProvider extends AbstractTimeSampleProvider { + public HuamiSpo2SampleProvider(final GBDevice device, final DaoSession session) { + super(device, session); + } + + @NonNull + @Override + public AbstractDao getSampleDao() { + return getSession().getHuamiSpo2SampleDao(); + } + + @NonNull + @Override + protected Property getTimestampSampleProperty() { + return HuamiSpo2SampleDao.Properties.Timestamp; + } + + @NonNull + @Override + protected Property getDeviceIdentifierSampleProperty() { + return HuamiSpo2SampleDao.Properties.DeviceId; + } + + @Override + public HuamiSpo2Sample createSample() { + return new HuamiSpo2Sample(); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/HuamiStressSampleProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/HuamiStressSampleProvider.java new file mode 100644 index 000000000..acc8f66c6 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/HuamiStressSampleProvider.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.HuamiStressSample; +import nodomain.freeyourgadget.gadgetbridge.entities.HuamiStressSampleDao; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; + +public class HuamiStressSampleProvider extends AbstractTimeSampleProvider { + public HuamiStressSampleProvider(final GBDevice device, final DaoSession session) { + super(device, session); + } + + @NonNull + @Override + public AbstractDao getSampleDao() { + return getSession().getHuamiStressSampleDao(); + } + + @NonNull + @Override + protected Property getTimestampSampleProperty() { + return HuamiStressSampleDao.Properties.Timestamp; + } + + @NonNull + @Override + protected Property getDeviceIdentifierSampleProperty() { + return HuamiStressSampleDao.Properties.DeviceId; + } + + @Override + public HuamiStressSample createSample() { + return new HuamiStressSample(); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfitband5/AmazfitBand5Coordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfitband5/AmazfitBand5Coordinator.java index 577bb30d6..a3f83dfdb 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfitband5/AmazfitBand5Coordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfitband5/AmazfitBand5Coordinator.java @@ -80,6 +80,11 @@ public class AmazfitBand5Coordinator extends HuamiCoordinator { return true; } + @Override + public boolean supportsStressMeasurement() { + return true; + } + @Override public boolean supportsMusicInfo() { return true; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/miband5/MiBand5Coordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/miband5/MiBand5Coordinator.java index 645e63384..db12c21c2 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/miband5/MiBand5Coordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/miband5/MiBand5Coordinator.java @@ -84,6 +84,11 @@ public class MiBand5Coordinator extends HuamiCoordinator { return true; } + @Override + public boolean supportsStressMeasurement() { + return true; + } + @Override public boolean supportsMusicInfo() { return true; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/entities/AbstractHeartRateSample.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/entities/AbstractHeartRateSample.java new file mode 100644 index 000000000..f4472dbcb --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/entities/AbstractHeartRateSample.java @@ -0,0 +1,37 @@ +/* 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.devices.TimeSampleProvider; +import nodomain.freeyourgadget.gadgetbridge.model.HeartRateSample; +import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils; + +public abstract class AbstractHeartRateSample extends AbstractTimeSample implements HeartRateSample { + @NonNull + @Override + public String toString() { + 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/entities/AbstractSpo2Sample.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/entities/AbstractSpo2Sample.java new file mode 100644 index 000000000..8b73972a0 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/entities/AbstractSpo2Sample.java @@ -0,0 +1,48 @@ +/* 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.Spo2Sample; +import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils; + +public abstract class AbstractSpo2Sample extends AbstractTimeSample implements Spo2Sample { + public abstract int getTypeNum(); + public abstract void setTypeNum(int num); + + @Override + public Type getType() { + return Type.fromNum(getTypeNum()); + } + + public void setType(final Type type) { + setTypeNum(type.getNum()); + } + + @NonNull + @Override + public String toString() { + return getClass().getSimpleName() + "{" + + "timestamp=" + DateTimeUtils.formatDateTime(DateTimeUtils.parseTimestampMillis(getTimestamp())) + + ", spo2=" + getSpo2() + + ", type=" + getType() + + ", userId=" + getUserId() + + ", deviceId=" + getDeviceId() + + "}"; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/entities/AbstractStressSample.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/entities/AbstractStressSample.java new file mode 100644 index 000000000..91ff6fe01 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/entities/AbstractStressSample.java @@ -0,0 +1,48 @@ +/* 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.StressSample; +import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils; + +public abstract class AbstractStressSample extends AbstractTimeSample implements StressSample { + public abstract int getTypeNum(); + public abstract void setTypeNum(int num); + + @Override + public Type getType() { + return Type.fromNum(getTypeNum()); + } + + public void setType(final Type type) { + setTypeNum(type.getNum()); + } + + @NonNull + @Override + public String toString() { + return getClass().getSimpleName() + "{" + + "timestamp=" + DateTimeUtils.formatDateTime(DateTimeUtils.parseTimestampMillis(getTimestamp())) + + ", stress=" + getStress() + + ", 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 new file mode 100644 index 000000000..0860ab04f --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/HeartRateSample.java @@ -0,0 +1,32 @@ +/* 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 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. + */ + int getHeartRate(); +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/Spo2Sample.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/Spo2Sample.java new file mode 100644 index 000000000..67a8c14dc --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/Spo2Sample.java @@ -0,0 +1,55 @@ +/* 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 Spo2Sample extends TimeSample { + enum Type { + MANUAL(0), + AUTOMATIC(1), + ; + + private final int num; + + Type(final int num) { + this.num = num; + } + + public int getNum() { + return num; + } + + public static Type fromNum(final int num) { + for (Type value : Type.values()) { + if (value.getNum() == num) { + return value; + } + } + + throw new IllegalArgumentException("Unknown num " + num); + } + } + + /** + * Returns the measurement type for this SpO2 value. + */ + Type getType(); + + /** + * Returns the SpO2 value between 0 and 100%. + */ + int getSpo2(); +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/StressSample.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/StressSample.java new file mode 100644 index 000000000..807d414cf --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/StressSample.java @@ -0,0 +1,59 @@ +/* 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 StressSample extends TimeSample { + enum Type { + MANUAL(0), + AUTOMATIC(1), + ; + + private final int num; + + Type(final int num) { + this.num = num; + } + + public int getNum() { + return num; + } + + public static Type fromNum(final int num) { + for (Type value : Type.values()) { + if (value.getNum() == num) { + return value; + } + } + + throw new IllegalArgumentException("Unknown num " + num); + } + } + + /** + * Returns the measurement type for this stress value. + */ + Type getType(); + + /** + * Returns the normalized stress value between 0 and 100: + * - 0-39 = relaxed + * - 40-59 = mild + * - 60-79 = moderate + * - 80-100 = high + */ + int getStress(); +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/AbstractRepeatingFetchOperation.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/AbstractRepeatingFetchOperation.java index b87415bb2..4fc3efd06 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/AbstractRepeatingFetchOperation.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/AbstractRepeatingFetchOperation.java @@ -61,20 +61,20 @@ public abstract class AbstractRepeatingFetchOperation extends AbstractFetchOpera @Override protected void startFetching(final TransactionBuilder builder) { - LOG.info("start {}", getName()); final GregorianCalendar sinceWhen = getLastSuccessfulSyncTime(); + LOG.info("start {} since {}", getName(), sinceWhen.getTime()); startFetching(builder, dataType, sinceWhen); } /** - * Handle the buffered activity data. + * Handle the buffered data. * * @param timestamp The timestamp of the first sample. This function should update this to the * timestamp of the last processed sample. * @param bytes the buffered bytes * @return true on success */ - protected abstract boolean handleActivityData(final GregorianCalendar timestamp, final byte[] bytes); + protected abstract boolean handleActivityData(GregorianCalendar timestamp, byte[] bytes); @Override protected boolean handleActivityFetchFinish(final boolean success) { @@ -186,7 +186,7 @@ public abstract class AbstractRepeatingFetchOperation extends AbstractFetchOpera return true; } - public void dumpBytesToExternalStorage(final byte[] bytes, final GregorianCalendar timestamp) { + protected void dumpBytesToExternalStorage(final byte[] bytes, final GregorianCalendar timestamp) { try { final File externalFilesDir = FileUtils.getExternalFilesDir(); final File targetDir = new File(externalFilesDir, "rawFetchOperations"); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/FetchSpo2NormalOperation.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/FetchSpo2NormalOperation.java index a4dc78dc7..0599dbc2f 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/FetchSpo2NormalOperation.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/FetchSpo2NormalOperation.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.HuamiSpo2SampleProvider; +import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; +import nodomain.freeyourgadget.gadgetbridge.entities.Device; +import nodomain.freeyourgadget.gadgetbridge.entities.HuamiSpo2Sample; +import nodomain.freeyourgadget.gadgetbridge.entities.User; +import nodomain.freeyourgadget.gadgetbridge.model.Spo2Sample; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiSupport; +import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper; import nodomain.freeyourgadget.gadgetbridge.util.GB; /** @@ -52,19 +67,51 @@ public class FetchSpo2NormalOperation extends AbstractRepeatingFetchOperation { return false; } + final List samples = new ArrayList<>(); + while (buf.position() < bytes.length) { final long timestampSeconds = buf.getInt(); final byte spo2raw = buf.get(); final boolean autoMeasurement = (spo2raw < 0); - final byte spo2 = (byte) (autoMeasurement ? (spo2raw + 128) : spo2raw); + final byte spo2 = (byte) (spo2raw < 0 ? spo2raw + 128 : spo2raw); final byte[] unknown = new byte[60]; // starts with a few spo2 values, but mostly zeroes after? buf.get(unknown); timestamp.setTimeInMillis(timestampSeconds * 1000L); - LOG.info("SPO2 at {}: {} auto={} unknown={}", timestamp.getTime(), spo2, autoMeasurement, GB.hexdump(unknown)); - // TODO save + LOG.trace("SPO2 at {}: {} auto={}", timestamp.getTime(), spo2, autoMeasurement); + + final HuamiSpo2Sample sample = new HuamiSpo2Sample(); + sample.setTimestamp(timestamp.getTimeInMillis()); + sample.setType(autoMeasurement ? Spo2Sample.Type.AUTOMATIC : Spo2Sample.Type.MANUAL); + sample.setSpo2(spo2); + 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 HuamiSpo2SampleProvider sampleProvider = coordinator.getSpo2SampleProvider(getDevice(), session); + + for (final HuamiSpo2Sample sample : samples) { + sample.setDevice(device); + sample.setUser(user); + } + + LOG.debug("Will persist {} normal spo2 samples", samples.size()); + sampleProvider.addSamples(samples); + } catch (final Exception e) { + GB.toast(getContext(), "Error saving normal spo2 samples", Toast.LENGTH_LONG, GB.ERROR, e); + return false; } return true; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/FetchSpo2SleepOperation.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/FetchSpo2SleepOperation.java index af0970465..d2d91f93f 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/FetchSpo2SleepOperation.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/FetchSpo2SleepOperation.java @@ -69,7 +69,7 @@ public class FetchSpo2SleepOperation extends AbstractRepeatingFetchOperation { timestamp.setTimeInMillis(timestampSeconds * 1000L); - LOG.info("SPO2 (sleep) at {}: {} unknown={}", timestamp.getTime(), spo2, GB.hexdump(unknown)); + LOG.debug("SPO2 (sleep) at {}: {} unknown={}", timestamp.getTime(), spo2, GB.hexdump(unknown)); // TODO save } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/FetchStressAutoOperation.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/FetchStressAutoOperation.java index bf025bd7c..8ea213a4b 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/FetchStressAutoOperation.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/FetchStressAutoOperation.java @@ -16,14 +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.util.ArrayList; import java.util.Calendar; 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.HuamiStressSampleProvider; +import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; +import nodomain.freeyourgadget.gadgetbridge.entities.Device; +import nodomain.freeyourgadget.gadgetbridge.entities.HuamiStressSample; +import nodomain.freeyourgadget.gadgetbridge.entities.User; +import nodomain.freeyourgadget.gadgetbridge.model.StressSample; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiSupport; +import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper; +import nodomain.freeyourgadget.gadgetbridge.util.GB; /** * An operation that fetches auto stress data. @@ -37,6 +53,8 @@ public class FetchStressAutoOperation extends AbstractRepeatingFetchOperation { @Override protected boolean handleActivityData(final GregorianCalendar timestamp, final byte[] bytes) { + final List samples = new ArrayList<>(); + for (byte b : bytes) { timestamp.add(Calendar.MINUTE, 1); @@ -44,16 +62,44 @@ public class FetchStressAutoOperation extends AbstractRepeatingFetchOperation { continue; } - final int stress = b & 0xff; - // 0-39 = relaxed // 40-59 = mild // 60-79 = moderate // 80-100 = high + final int stress = b & 0xff; - LOG.info("Stress (auto) at {}: {}", timestamp.getTime(), stress); + LOG.trace("Stress (auto) at {}: {}", timestamp.getTime(), stress); - // TODO: Save stress data + final HuamiStressSample sample = new HuamiStressSample(); + sample.setTimestamp(timestamp.getTimeInMillis()); + sample.setType(StressSample.Type.AUTOMATIC); + sample.setStress(stress); + 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 HuamiStressSampleProvider sampleProvider = coordinator.getStressSampleProvider(getDevice(), session); + + for (final HuamiStressSample sample : samples) { + sample.setDevice(device); + sample.setUser(user); + } + + LOG.debug("Will persist {} auto stress samples", samples.size()); + sampleProvider.addSamples(samples); + } catch (final Exception e) { + GB.toast(getContext(), "Error saving auto stress samples", Toast.LENGTH_LONG, GB.ERROR, e); + return false; } return true; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/FetchStressManualOperation.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/FetchStressManualOperation.java index e6c276a29..90ef0e0f7 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/FetchStressManualOperation.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/FetchStressManualOperation.java @@ -16,16 +16,32 @@ 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.HuamiStressSampleProvider; +import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; +import nodomain.freeyourgadget.gadgetbridge.entities.Device; +import nodomain.freeyourgadget.gadgetbridge.entities.HuamiStressSample; +import nodomain.freeyourgadget.gadgetbridge.entities.User; +import nodomain.freeyourgadget.gadgetbridge.model.StressSample; 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 stress data. @@ -47,6 +63,8 @@ public class FetchStressManualOperation extends AbstractRepeatingFetchOperation final ByteBuffer buffer = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN); final GregorianCalendar lastSyncTimestamp = new GregorianCalendar(); + final List samples = new ArrayList<>(); + while (buffer.position() < bytes.length) { final long currentTimestamp = BLETypeConversions.toUnsigned(buffer.getInt()) * 1000; @@ -57,9 +75,38 @@ public class FetchStressManualOperation extends AbstractRepeatingFetchOperation final int stress = buffer.get() & 0xff; timestamp.setTimeInMillis(currentTimestamp); - LOG.info("Stress (manual) at {}: {}", lastSyncTimestamp.getTime(), stress); + LOG.trace("Stress (manual) at {}: {}", lastSyncTimestamp.getTime(), stress); - // TODO: Save stress data + final HuamiStressSample sample = new HuamiStressSample(); + sample.setTimestamp(timestamp.getTimeInMillis()); + sample.setType(StressSample.Type.MANUAL); + sample.setStress(stress); + 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 HuamiStressSampleProvider sampleProvider = coordinator.getStressSampleProvider(getDevice(), session); + + for (final HuamiStressSample sample : samples) { + sample.setDevice(device); + sample.setUser(user); + } + + LOG.debug("Will persist {} manual stress samples", samples.size()); + sampleProvider.addSamples(samples); + } catch (final Exception e) { + GB.toast(getContext(), "Error saving manual stress samples", Toast.LENGTH_LONG, GB.ERROR, e); + return false; } return true;