From a02f5aaf82dd3eb427f851e6e2c71576eaa22e4c Mon Sep 17 00:00:00 2001 From: Arjan Schrijver Date: Mon, 23 Dec 2024 21:15:05 +0100 Subject: [PATCH] Colmi R09: Add support for temperature data --- .../gadgetbridge/daogen/GBDaoGenerator.java | 10 +++- .../colmi/AbstractColmiR0xCoordinator.java | 7 +++ .../devices/colmi/ColmiR09Coordinator.java | 16 +++++ .../devices/colmi/ColmiR0xConstants.java | 1 + .../devices/colmi/ColmiR0xPacketHandler.java | 60 +++++++++++++++++++ .../ColmiTemperatureSampleProvider.java | 58 ++++++++++++++++++ .../devices/colmi/ColmiR0xDeviceSupport.java | 26 +++++++- 7 files changed, 176 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/colmi/samples/ColmiTemperatureSampleProvider.java diff --git a/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java b/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java index 73cd6e2f8..770cf142f 100644 --- a/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java +++ b/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java @@ -54,7 +54,7 @@ public class GBDaoGenerator { public static void main(String[] args) throws Exception { - final Schema schema = new Schema(92, MAIN_PACKAGE + ".entities"); + final Schema schema = new Schema(93, MAIN_PACKAGE + ".entities"); Entity userAttributes = addUserAttributes(schema); Entity user = addUserInfo(schema, userAttributes); @@ -150,6 +150,7 @@ public class GBDaoGenerator { addColmiSleepStageSample(schema, user, device); addColmiHrvValueSample(schema, user, device); addColmiHrvSummarySample(schema, user, device); + addColmiTemperatureSample(schema, user, device); addHuaweiActivitySample(schema, user, device); @@ -591,6 +592,13 @@ public class GBDaoGenerator { return hrvSummarySample; } + private static Entity addColmiTemperatureSample(Schema schema, Entity user, Entity device) { + Entity sample = addEntity(schema, "ColmiTemperatureSample"); + addCommonTimeSampleProperties("AbstractTemperatureSample", sample, user, device); + addTemperatureProperties(sample); + return sample; + } + 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/colmi/AbstractColmiR0xCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/colmi/AbstractColmiR0xCoordinator.java index 1a6136c6d..3c0032d2a 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/colmi/AbstractColmiR0xCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/colmi/AbstractColmiR0xCoordinator.java @@ -41,6 +41,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.colmi.samples.ColmiHrvSummar import nodomain.freeyourgadget.gadgetbridge.devices.colmi.samples.ColmiHrvValueSampleProvider; import nodomain.freeyourgadget.gadgetbridge.devices.colmi.samples.ColmiSpo2SampleProvider; import nodomain.freeyourgadget.gadgetbridge.devices.colmi.samples.ColmiStressSampleProvider; +import nodomain.freeyourgadget.gadgetbridge.devices.colmi.samples.ColmiTemperatureSampleProvider; import nodomain.freeyourgadget.gadgetbridge.entities.ColmiActivitySampleDao; import nodomain.freeyourgadget.gadgetbridge.entities.ColmiHeartRateSampleDao; import nodomain.freeyourgadget.gadgetbridge.entities.ColmiHrvSummarySampleDao; @@ -57,6 +58,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.HrvSummarySample; import nodomain.freeyourgadget.gadgetbridge.model.HrvValueSample; import nodomain.freeyourgadget.gadgetbridge.model.Spo2Sample; import nodomain.freeyourgadget.gadgetbridge.model.StressSample; +import nodomain.freeyourgadget.gadgetbridge.model.TemperatureSample; import nodomain.freeyourgadget.gadgetbridge.service.DeviceSupport; import nodomain.freeyourgadget.gadgetbridge.service.devices.colmi.ColmiR0xDeviceSupport; @@ -201,6 +203,11 @@ public abstract class AbstractColmiR0xCoordinator extends AbstractBLEDeviceCoord return new ColmiHrvValueSampleProvider(device, session); } + @Override + public TimeSampleProvider getTemperatureSampleProvider(GBDevice device, DaoSession session) { + return new ColmiTemperatureSampleProvider(device, session); + } + @Override public List getHeartRateMeasurementIntervals() { return Arrays.asList( diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/colmi/ColmiR09Coordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/colmi/ColmiR09Coordinator.java index 8a27e0d47..0933ab4f0 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/colmi/ColmiR09Coordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/colmi/ColmiR09Coordinator.java @@ -22,6 +22,12 @@ import org.slf4j.LoggerFactory; import java.util.regex.Pattern; import nodomain.freeyourgadget.gadgetbridge.R; +import nodomain.freeyourgadget.gadgetbridge.devices.TimeSampleProvider; +import nodomain.freeyourgadget.gadgetbridge.devices.colmi.samples.ColmiStressSampleProvider; +import nodomain.freeyourgadget.gadgetbridge.devices.colmi.samples.ColmiTemperatureSampleProvider; +import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.model.TemperatureSample; public class ColmiR09Coordinator extends AbstractColmiR0xCoordinator { private static final Logger LOG = LoggerFactory.getLogger(ColmiR09Coordinator.class); @@ -35,4 +41,14 @@ public class ColmiR09Coordinator extends AbstractColmiR0xCoordinator { public int getDeviceNameResource() { return R.string.devicetype_colmi_r09; } + + @Override + public boolean supportsTemperatureMeasurement() { + return true; + } + + @Override + public boolean supportsContinuousTemperature() { + return true; + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/colmi/ColmiR0xConstants.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/colmi/ColmiR0xConstants.java index fd80e8cbb..8db709715 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/colmi/ColmiR0xConstants.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/colmi/ColmiR0xConstants.java @@ -57,6 +57,7 @@ public class ColmiR0xConstants { public static final byte NOTIFICATION_BATTERY_LEVEL = 0x0c; public static final byte NOTIFICATION_LIVE_ACTIVITY = 0x12; + public static final byte BIG_DATA_TYPE_TEMPERATURE = 0x25; public static final byte BIG_DATA_TYPE_SLEEP = 0x27; public static final byte BIG_DATA_TYPE_SPO2 = 0x2a; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/colmi/ColmiR0xPacketHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/colmi/ColmiR0xPacketHandler.java index d7d06efd0..db19071ac 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/colmi/ColmiR0xPacketHandler.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/colmi/ColmiR0xPacketHandler.java @@ -42,6 +42,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.colmi.samples.ColmiSleepSess import nodomain.freeyourgadget.gadgetbridge.devices.colmi.samples.ColmiSleepStageSampleProvider; import nodomain.freeyourgadget.gadgetbridge.devices.colmi.samples.ColmiSpo2SampleProvider; import nodomain.freeyourgadget.gadgetbridge.devices.colmi.samples.ColmiStressSampleProvider; +import nodomain.freeyourgadget.gadgetbridge.devices.colmi.samples.ColmiTemperatureSampleProvider; import nodomain.freeyourgadget.gadgetbridge.entities.ColmiActivitySample; import nodomain.freeyourgadget.gadgetbridge.entities.ColmiHeartRateSample; import nodomain.freeyourgadget.gadgetbridge.entities.ColmiHrvValueSample; @@ -49,6 +50,7 @@ import nodomain.freeyourgadget.gadgetbridge.entities.ColmiSleepSessionSample; import nodomain.freeyourgadget.gadgetbridge.entities.ColmiSleepStageSample; import nodomain.freeyourgadget.gadgetbridge.entities.ColmiSpo2Sample; import nodomain.freeyourgadget.gadgetbridge.entities.ColmiStressSample; +import nodomain.freeyourgadget.gadgetbridge.entities.ColmiTemperatureSample; import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; import nodomain.freeyourgadget.gadgetbridge.entities.Device; import nodomain.freeyourgadget.gadgetbridge.entities.User; @@ -488,4 +490,62 @@ public class ColmiR0xPacketHandler { } } } + + public static void historicalTemperature(GBDevice device, byte[] value) { + ArrayList temperatureSamples = new ArrayList<>(); + int length = BLETypeConversions.toUint16(value[2], value[3]); + int index = 6; // start of data (day nr, followed by values) + int days_ago = -1; + while (days_ago != 0 && index - 6 < length) { + days_ago = value[index]; + Calendar syncingDay = Calendar.getInstance(); + syncingDay.add(Calendar.DAY_OF_MONTH, 0 - days_ago); + syncingDay.set(Calendar.MINUTE, 0); + syncingDay.set(Calendar.SECOND, 0); + syncingDay.set(Calendar.MILLISECOND, 0); + index++; + index++; // Skip one extra unknown byte (so far always observed to be 0x1e) + for (int hour=0; hour<=23; hour++) { + syncingDay.set(Calendar.HOUR_OF_DAY, hour); + syncingDay.set(Calendar.MINUTE, 0); + float temp_00 = value[index]; + index++; + float temp_30 = value[index]; + index++; + if (temp_00 > 0) { + LOG.info("Received temperature data from {} days ago at {}:00: {} °C", days_ago, hour, temp_00); + ColmiTemperatureSample temperatureSample = new ColmiTemperatureSample(); + temperatureSample.setTimestamp(syncingDay.getTimeInMillis()); + temperatureSample.setTemperature((temp_00 / 10) + 20); + temperatureSamples.add(temperatureSample); + } + syncingDay.set(Calendar.MINUTE, 30); + if (temp_30 > 0) { + LOG.info("Received temperature data from {} days ago at {}:30: {} °C", days_ago, hour, temp_30); + ColmiTemperatureSample temperatureSample = new ColmiTemperatureSample(); + temperatureSample.setTimestamp(syncingDay.getTimeInMillis()); + temperatureSample.setTemperature((temp_30 / 10) + 20); + temperatureSamples.add(temperatureSample); + } + if (index - 6 >= length) { + break; + } + } + } + if (!temperatureSamples.isEmpty()) { + try (DBHandler db = GBApplication.acquireDB()) { + ColmiTemperatureSampleProvider sampleProvider = new ColmiTemperatureSampleProvider(device, db.getDaoSession()); + Long userId = DBHelper.getUser(db.getDaoSession()).getId(); + Long deviceId = DBHelper.getDevice(device, db.getDaoSession()).getId(); + for (final ColmiTemperatureSample sample : temperatureSamples) { + sample.setDeviceId(deviceId); + sample.setUserId(userId); + } + LOG.info("Will persist {} temperature samples", temperatureSamples.size()); + sampleProvider.addSamples(temperatureSamples); + } catch (Exception e) { + LOG.error("Error acquiring database for recording temperature samples", e); + } + } + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/colmi/samples/ColmiTemperatureSampleProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/colmi/samples/ColmiTemperatureSampleProvider.java new file mode 100644 index 000000000..4a1341555 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/colmi/samples/ColmiTemperatureSampleProvider.java @@ -0,0 +1,58 @@ +/* Copyright (C) 2024 Arjan Schrijver + + 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.colmi.samples; + +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.ColmiHrvValueSample; +import nodomain.freeyourgadget.gadgetbridge.entities.ColmiHrvValueSampleDao; +import nodomain.freeyourgadget.gadgetbridge.entities.ColmiTemperatureSample; +import nodomain.freeyourgadget.gadgetbridge.entities.ColmiTemperatureSampleDao; +import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; + +public class ColmiTemperatureSampleProvider extends AbstractTimeSampleProvider { + public ColmiTemperatureSampleProvider(final GBDevice device, final DaoSession session) { + super(device, session); + } + + @NonNull + @Override + public AbstractDao getSampleDao() { + return getSession().getColmiTemperatureSampleDao(); + } + + @NonNull + @Override + protected Property getTimestampSampleProperty() { + return ColmiTemperatureSampleDao.Properties.Timestamp; + } + + @NonNull + @Override + protected Property getDeviceIdentifierSampleProperty() { + return ColmiTemperatureSampleDao.Properties.DeviceId; + } + + @Override + public ColmiTemperatureSample createSample() { + return new ColmiTemperatureSample(); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/colmi/ColmiR0xDeviceSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/colmi/ColmiR0xDeviceSupport.java index 984a1c649..00a479172 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/colmi/ColmiR0xDeviceSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/colmi/ColmiR0xDeviceSupport.java @@ -296,7 +296,11 @@ public class ColmiR0xDeviceSupport extends AbstractBTLEDeviceSupport { daysAgo++; fetchHistoryHRV(); } else { - fetchRecordedDataFinished(); + if (getDevice().getDeviceCoordinator().supportsTemperatureMeasurement()) { + fetchTemperature(); + } else { + fetchRecordedDataFinished(); + } } } break; @@ -370,6 +374,10 @@ public class ColmiR0xDeviceSupport extends AbstractBTLEDeviceSupport { return true; } switch (value[1]) { + case ColmiR0xConstants.BIG_DATA_TYPE_TEMPERATURE: + ColmiR0xPacketHandler.historicalTemperature(getDevice(), value); + fetchRecordedDataFinished(); + break; case ColmiR0xConstants.BIG_DATA_TYPE_SLEEP: ColmiR0xPacketHandler.historicalSleep(getDevice(), getContext(), value); @@ -712,4 +720,20 @@ public class ColmiR0xDeviceSupport extends AbstractBTLEDeviceSupport { LOG.info("Fetch historical HRV data request sent ({}): {}", syncingDay.getTime(), StringUtils.bytesToHex(hrvHistoryRequest)); sendWrite("hrvHistoryRequest", hrvHistoryRequest); } + + private void fetchTemperature() { + getDevice().setBusyTask(getContext().getString(R.string.busy_task_fetch_temperature)); + getDevice().sendDeviceUpdateIntent(getContext()); + byte[] temperatureHistoryRequest = new byte[]{ + ColmiR0xConstants.CMD_BIG_DATA_V2, + ColmiR0xConstants.BIG_DATA_TYPE_TEMPERATURE, + 0x01, + 0x00, + 0x3e, + (byte) 0x81, + 0x02 + }; + LOG.info("Fetch historical temperature data request sent: {}", StringUtils.bytesToHex(temperatureHistoryRequest)); + sendCommand("temperatureHistoryRequest", temperatureHistoryRequest); + } }