From 6b2cb05027984c78beea1a2ab794c85904936b2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Rebelo?= Date: Thu, 18 Jan 2024 21:06:40 +0000 Subject: [PATCH] Xiaomi: Fetch manual samples --- .../gadgetbridge/daogen/GBDaoGenerator.java | 11 +- .../xiaomi/XiaomiManualSampleProvider.java | 61 +++++++++ .../xiaomi/activity/XiaomiActivityFileId.java | 1 + .../xiaomi/activity/XiaomiActivityParser.java | 7 + .../activity/impl/ManualSamplesParser.java | 121 ++++++++++++++++++ 5 files changed, 200 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/xiaomi/XiaomiManualSampleProvider.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/activity/impl/ManualSamplesParser.java diff --git a/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java b/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java index ce2001cd0..10883f697 100644 --- a/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java +++ b/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java @@ -45,7 +45,7 @@ public class GBDaoGenerator { public static void main(String[] args) throws Exception { - final Schema schema = new Schema(67, MAIN_PACKAGE + ".entities"); + final Schema schema = new Schema(68, MAIN_PACKAGE + ".entities"); Entity userAttributes = addUserAttributes(schema); Entity user = addUserInfo(schema, userAttributes); @@ -73,6 +73,7 @@ public class GBDaoGenerator { addXiaomiActivitySample(schema, user, device); addXiaomiSleepTimeSamples(schema, user, device); addXiaomiSleepStageSamples(schema, user, device); + addXiaomiManualSamples(schema, user, device); addXiaomiDailySummarySamples(schema, user, device); addPebbleHealthActivitySample(schema, user, device); addPebbleHealthActivityKindOverlay(schema, user, device); @@ -367,6 +368,14 @@ public class GBDaoGenerator { return sample; } + private static Entity addXiaomiManualSamples(Schema schema, Entity user, Entity device) { + Entity sample = addEntity(schema, "XiaomiManualSample"); + addCommonTimeSampleProperties("AbstractTimeSample", sample, user, device); + sample.addIntProperty("type"); + sample.addIntProperty("value"); + return sample; + } + private static Entity addXiaomiDailySummarySamples(Schema schema, Entity user, Entity device) { Entity sample = addEntity(schema, "XiaomiDailySummarySample"); addCommonTimeSampleProperties("AbstractTimeSample", sample, user, device); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/xiaomi/XiaomiManualSampleProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/xiaomi/XiaomiManualSampleProvider.java new file mode 100644 index 000000000..7f73dcaa6 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/xiaomi/XiaomiManualSampleProvider.java @@ -0,0 +1,61 @@ +/* Copyright (C) 2024 José Rebelo + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.devices.xiaomi; + +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.XiaomiManualSample; +import nodomain.freeyourgadget.gadgetbridge.entities.XiaomiSleepStageSampleDao; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; + +public class XiaomiManualSampleProvider extends AbstractTimeSampleProvider { + public static final int TYPE_HR = 0x11; + public static final int TYPE_SPO2 = 0x12; + public static final int TYPE_STRESS = 0x13; + public static final int TYPE_TEMPERATURE = 0x44; + + public XiaomiManualSampleProvider(final GBDevice device, final DaoSession session) { + super(device, session); + } + + @NonNull + @Override + public AbstractDao getSampleDao() { + return getSession().getXiaomiManualSampleDao(); + } + + @NonNull + @Override + protected Property getTimestampSampleProperty() { + return XiaomiSleepStageSampleDao.Properties.Timestamp; + } + + @NonNull + @Override + protected Property getDeviceIdentifierSampleProperty() { + return XiaomiSleepStageSampleDao.Properties.DeviceId; + } + + @Override + public XiaomiManualSample createSample() { + return new XiaomiManualSample(); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/activity/XiaomiActivityFileId.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/activity/XiaomiActivityFileId.java index 400a70d39..f6571841f 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/activity/XiaomiActivityFileId.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/activity/XiaomiActivityFileId.java @@ -193,6 +193,7 @@ public class XiaomiActivityFileId implements Comparable { UNKNOWN(Type.UNKNOWN, -1), ACTIVITY_DAILY(Type.ACTIVITY, 0x00), ACTIVITY_SLEEP_STAGES(Type.ACTIVITY, 0x03), + ACTIVITY_MANUAL_SAMPLES(Type.ACTIVITY, 0x06), ACTIVITY_SLEEP(Type.ACTIVITY, 0x08), SPORTS_OUTDOOR_RUNNING(Type.SPORTS, 0x01), SPORTS_OUTDOOR_WALKING_V1(Type.SPORTS, 0x02), diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/activity/XiaomiActivityParser.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/activity/XiaomiActivityParser.java index 4ab0cc0a6..8a759ac9e 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/activity/XiaomiActivityParser.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/activity/XiaomiActivityParser.java @@ -31,6 +31,7 @@ import nodomain.freeyourgadget.gadgetbridge.entities.Device; import nodomain.freeyourgadget.gadgetbridge.entities.User; import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind; import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.XiaomiSupport; +import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.activity.impl.ManualSamplesParser; import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.activity.impl.DailyDetailsParser; import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.activity.impl.DailySummaryParser; import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.activity.impl.SleepDetailsParser; @@ -102,6 +103,12 @@ public abstract class XiaomiActivityParser { return new SleepStagesParser(); } + break; + case ACTIVITY_MANUAL_SAMPLES: + if (fileId.getDetailType() == XiaomiActivityFileId.DetailType.DETAILS) { + return new ManualSamplesParser(); + } + break; case ACTIVITY_SLEEP: if (fileId.getDetailType() == XiaomiActivityFileId.DetailType.DETAILS) { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/activity/impl/ManualSamplesParser.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/activity/impl/ManualSamplesParser.java new file mode 100644 index 000000000..1a51d7499 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/activity/impl/ManualSamplesParser.java @@ -0,0 +1,121 @@ +/* Copyright (C) 2024 José Rebelo + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.activity.impl; + +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.List; + +import nodomain.freeyourgadget.gadgetbridge.GBApplication; +import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; +import nodomain.freeyourgadget.gadgetbridge.database.DBHelper; +import nodomain.freeyourgadget.gadgetbridge.devices.xiaomi.XiaomiManualSampleProvider; +import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; +import nodomain.freeyourgadget.gadgetbridge.entities.Device; +import nodomain.freeyourgadget.gadgetbridge.entities.User; +import nodomain.freeyourgadget.gadgetbridge.entities.XiaomiManualSample; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.XiaomiSupport; +import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.activity.XiaomiActivityFileId; +import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.activity.XiaomiActivityParser; +import nodomain.freeyourgadget.gadgetbridge.util.GB; + +public class ManualSamplesParser extends XiaomiActivityParser { + private static final Logger LOG = LoggerFactory.getLogger(ManualSamplesParser.class); + + @Override + public boolean parse(final XiaomiSupport support, final XiaomiActivityFileId fileId, final byte[] bytes) { + if (fileId.getVersion() != 2) { + LOG.warn("Unknown manual samples version {}", fileId.getVersion()); + return false; + } + + final ByteBuffer buf = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN); + + // Looks like there is no header, it starts right away with samples: + // 8A90A965 12 63 <- spo2 + // ... multiple 13 00 + // C793A965 13 00 + // 9698A965 13 1C <- stress + // E79CA965 44 5A0E0000 <- body temperature + // 729FA965 44 590E0000 <- body temperature + + final List samples = new ArrayList<>(); + + while (buf.position() < buf.limit()) { + final int timestamp = buf.getInt(); + final int type = buf.get() & 0xff; + + final int value; + switch (type) { + case XiaomiManualSampleProvider.TYPE_HR: + case XiaomiManualSampleProvider.TYPE_SPO2: + case XiaomiManualSampleProvider.TYPE_STRESS: + value = buf.get() & 0xff; + break; + case XiaomiManualSampleProvider.TYPE_TEMPERATURE: + value = buf.getInt(); + break; + default: + LOG.warn("Unknown sample type {}", type); + // We need to abort parsing, as we don't know the sample size + return false; + } + + if (value == 0) { + continue; + } + + LOG.debug("Got manual sample: ts={} type={} value={}", timestamp, type, value); + + final XiaomiManualSample sample = new XiaomiManualSample(); + sample.setTimestamp(timestamp * 1000L); + sample.setType(type); + sample.setValue(value); + + samples.add(sample); + } + + try (DBHandler handler = GBApplication.acquireDB()) { + final DaoSession session = handler.getDaoSession(); + + final GBDevice gbDevice = support.getDevice(); + final Device device = DBHelper.getDevice(gbDevice, session); + final User user = DBHelper.getUser(session); + + for (final XiaomiManualSample sample : samples) { + sample.setDevice(device); + sample.setUser(user); + } + + final XiaomiManualSampleProvider sampleProvider = new XiaomiManualSampleProvider(gbDevice, session); + sampleProvider.addSamples(samples); + } catch (final Exception e) { + GB.toast(support.getContext(), "Error saving manual samples", Toast.LENGTH_LONG, GB.ERROR); + LOG.error("Error saving manual samples", e); + return false; + } + + return true; + } +}