diff --git a/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java b/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java index 50c5f0057..02a86ce21 100644 --- a/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java +++ b/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java @@ -66,6 +66,7 @@ public class GBDaoGenerator { addHuamiHeartRateManualSample(schema, user, device); addHuamiHeartRateMaxSample(schema, user, device); addHuamiHeartRateRestingSample(schema, user, device); + addHuamiPaiSample(schema, user, device); addPebbleHealthActivitySample(schema, user, device); addPebbleHealthActivityKindOverlay(schema, user, device); addPebbleMisfitActivitySample(schema, user, device); @@ -284,6 +285,21 @@ public class GBDaoGenerator { return hrRestingSample; } + private static Entity addHuamiPaiSample(Schema schema, Entity user, Entity device) { + Entity paiSample = addEntity(schema, "HuamiPaiSample"); + addCommonTimeSampleProperties("AbstractPaiSample", paiSample, user, device); + paiSample.addIntProperty("utcOffset").notNull(); + paiSample.addFloatProperty("paiLow").notNull().codeBeforeGetter(OVERRIDE); + paiSample.addFloatProperty("paiModerate").notNull().codeBeforeGetter(OVERRIDE); + paiSample.addFloatProperty("paiHigh").notNull().codeBeforeGetter(OVERRIDE); + paiSample.addIntProperty("timeLow").notNull().codeBeforeGetter(OVERRIDE); + paiSample.addIntProperty("timeModerate").notNull().codeBeforeGetter(OVERRIDE); + paiSample.addIntProperty("timeHigh").notNull().codeBeforeGetter(OVERRIDE); + paiSample.addFloatProperty("paiToday").notNull().codeBeforeGetter(OVERRIDE); + paiSample.addFloatProperty("paiTotal").notNull().codeBeforeGetter(OVERRIDE); + return paiSample; + } + 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 ad15d3a56..6c2071e02 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractDeviceCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractDeviceCoordinator.java @@ -57,6 +57,7 @@ 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.PaiSample; import nodomain.freeyourgadget.gadgetbridge.model.Spo2Sample; import nodomain.freeyourgadget.gadgetbridge.model.StressSample; import nodomain.freeyourgadget.gadgetbridge.util.Prefs; @@ -179,6 +180,11 @@ public abstract class AbstractDeviceCoordinator implements DeviceCoordinator { return null; } + @Override + public TimeSampleProvider getPaiSampleProvider(GBDevice device, DaoSession session) { + return null; + } + @Override @Nullable public ActivitySummaryParser getActivitySummaryParser(final GBDevice device) { @@ -266,6 +272,11 @@ public abstract class AbstractDeviceCoordinator implements DeviceCoordinator { return false; } + @Override + public boolean supportsPai() { + 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 ffe928091..060daf500 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceCoordinator.java @@ -42,6 +42,7 @@ 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.PaiSample; import nodomain.freeyourgadget.gadgetbridge.model.Spo2Sample; import nodomain.freeyourgadget.gadgetbridge.model.StressSample; @@ -218,6 +219,12 @@ public interface DeviceCoordinator { */ boolean supportsHeartRateStats(); + /** + * Returns true if PAI (Personal Activity Intelligence) measurement and fetching is supported by + * the device (with this coordinator). + */ + boolean supportsPai(); + /** * Returns true if activity data fetching is supported AND possible at this * very moment. This will consider the device state (being connected/disconnected/busy...) @@ -260,6 +267,11 @@ public interface DeviceCoordinator { */ TimeSampleProvider getHeartRateManualSampleProvider(GBDevice device, DaoSession session); + /** + * Returns the sample provider for PAI data, for the device being supported. + */ + TimeSampleProvider getPaiSampleProvider(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 7f7f492e4..6148beb30 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 @@ -127,6 +127,11 @@ public abstract class Huami2021Coordinator extends HuamiCoordinator { return true; } + @Override + public boolean supportsPai() { + 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 da8b95bfe..2b7f6a199 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 @@ -165,6 +165,11 @@ public abstract class HuamiCoordinator extends AbstractBLEDeviceCoordinator { return new HuamiHeartRateManualSampleProvider(device, session); } + @Override + public HuamiPaiSampleProvider getPaiSampleProvider(GBDevice device, DaoSession session) { + return new HuamiPaiSampleProvider(device, session); + } + @Override public ActivitySummaryParser getActivitySummaryParser(final GBDevice device) { return new HuamiActivitySummaryParser(); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/HuamiPaiSampleProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/HuamiPaiSampleProvider.java new file mode 100644 index 000000000..a0c49e768 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/HuamiPaiSampleProvider.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.HuamiPaiSample; +import nodomain.freeyourgadget.gadgetbridge.entities.HuamiPaiSampleDao; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; + +public class HuamiPaiSampleProvider extends AbstractTimeSampleProvider { + public HuamiPaiSampleProvider(final GBDevice device, final DaoSession session) { + super(device, session); + } + + @NonNull + @Override + public AbstractDao getSampleDao() { + return getSession().getHuamiPaiSampleDao(); + } + + @NonNull + @Override + protected Property getTimestampSampleProperty() { + return HuamiPaiSampleDao.Properties.Timestamp; + } + + @NonNull + @Override + protected Property getDeviceIdentifierSampleProperty() { + return HuamiPaiSampleDao.Properties.DeviceId; + } + + @Override + public HuamiPaiSample createSample() { + return new HuamiPaiSample(); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/entities/AbstractPaiSample.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/entities/AbstractPaiSample.java new file mode 100644 index 000000000..c418d5146 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/entities/AbstractPaiSample.java @@ -0,0 +1,42 @@ +/* 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.PaiSample; +import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils; + +public abstract class AbstractPaiSample extends AbstractTimeSample implements PaiSample { + @NonNull + @Override + public String toString() { + return getClass().getSimpleName() + "{" + + "timestamp=" + DateTimeUtils.formatDateTime(DateTimeUtils.parseTimestampMillis(getTimestamp())) + + ", paiLow=" + getPaiLow() + + ", paiModerate=" + getPaiModerate() + + ", paiHigh=" + getPaiHigh() + + ", timeLow=" + getTimeLow() + + ", timeModerate=" + getTimeModerate() + + ", timeHigh=" + getTimeHigh() + + ", paiToday=" + getPaiToday() + + ", paiTotal=" + getPaiTotal() + + ", userId=" + getUserId() + + ", deviceId=" + getDeviceId() + + "}"; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/PaiSample.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/PaiSample.java new file mode 100644 index 000000000..3a8247777 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/PaiSample.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.model; + +public interface PaiSample extends TimeSample { + float getPaiLow(); + + float getPaiModerate(); + + float getPaiHigh(); + + int getTimeLow(); + + int getTimeModerate(); + + int getTimeHigh(); + + float getPaiToday(); + + float getPaiTotal(); +} 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 d7006463c..e3bc2ef12 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/RecordedDataTypes.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/RecordedDataTypes.java @@ -26,6 +26,7 @@ public class RecordedDataTypes { 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_PAI = 0x00000100; public static final int TYPE_ALL = (int)0xffffffff; } 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 c6160700b..2261b9509 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 @@ -121,6 +121,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.Abs 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.FetchPaiOperation; 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; @@ -1681,6 +1682,10 @@ public abstract class HuamiSupport extends AbstractBTLEDeviceSupport implements this.fetchOperationQueue.add(new FetchHeartRateRestingOperation(this)); } + if ((dataTypes & RecordedDataTypes.TYPE_PAI) != 0 && coordinator.supportsPai()) { + this.fetchOperationQueue.add(new FetchPaiOperation(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/FetchPaiOperation.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/FetchPaiOperation.java index c0e6c8c5a..863212acc 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/FetchPaiOperation.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/FetchPaiOperation.java @@ -16,15 +16,29 @@ 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.HuamiPaiSampleProvider; +import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; +import nodomain.freeyourgadget.gadgetbridge.entities.Device; +import nodomain.freeyourgadget.gadgetbridge.entities.HuamiPaiSample; +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; /** @@ -39,6 +53,8 @@ public class FetchPaiOperation extends AbstractRepeatingFetchOperation { @Override protected boolean handleActivityData(final GregorianCalendar timestamp, final byte[] bytes) { + final List samples = new ArrayList<>(); + final ByteBuffer buf = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN); while (buf.position() < bytes.length) { @@ -68,7 +84,7 @@ public class FetchPaiOperation extends AbstractRepeatingFetchOperation { byte[] unknown2 = new byte[39]; buf.get(unknown2); - LOG.debug( + LOG.trace( "PAI at {} + {}: paiLow={} paiModerate={} paiHigh={} timeLow={} timeMid={} timeHigh={} paiToday={} paiTotal={} unknown1={} unknown2={}", timestamp.getTime(), utcOffsetInQuarterHours, paiLow, paiModerate, paiHigh, @@ -78,7 +94,43 @@ public class FetchPaiOperation extends AbstractRepeatingFetchOperation { GB.hexdump(unknown2) ); - // TODO save + final HuamiPaiSample sample = new HuamiPaiSample(); + sample.setTimestamp(timestamp.getTimeInMillis()); + sample.setUtcOffset(utcOffsetInQuarterHours * 900000); + sample.setPaiLow(paiLow); + sample.setPaiModerate(paiModerate); + sample.setPaiHigh(paiHigh); + sample.setTimeLow(timeLow); + sample.setTimeModerate(timeModerate); + sample.setTimeHigh(timeHigh); + sample.setPaiToday(paiToday); + sample.setPaiTotal(paiTotal); + 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 HuamiPaiSampleProvider sampleProvider = coordinator.getPaiSampleProvider(getDevice(), session); + + for (final HuamiPaiSample sample : samples) { + sample.setDevice(device); + sample.setUser(user); + } + + LOG.debug("Will persist {} pai samples", samples.size()); + sampleProvider.addSamples(samples); + } catch (final Exception e) { + GB.toast(getContext(), "Error saving pai samples", Toast.LENGTH_LONG, GB.ERROR, e); + return false; } return true;