Colmi R09: Add support for temperature data

This commit is contained in:
Arjan Schrijver 2024-12-23 21:15:05 +01:00
parent 22063f07e3
commit a02f5aaf82
7 changed files with 176 additions and 2 deletions

View File

@ -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);
}

View File

@ -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<? extends TemperatureSample> getTemperatureSampleProvider(GBDevice device, DaoSession session) {
return new ColmiTemperatureSampleProvider(device, session);
}
@Override
public List<HeartRateCapability.MeasurementInterval> getHeartRateMeasurementIntervals() {
return Arrays.asList(

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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<ColmiTemperatureSample> 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);
}
}
}
}

View File

@ -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 <https://www.gnu.org/licenses/>. */
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<ColmiTemperatureSample> {
public ColmiTemperatureSampleProvider(final GBDevice device, final DaoSession session) {
super(device, session);
}
@NonNull
@Override
public AbstractDao<ColmiTemperatureSample, ?> 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();
}
}

View File

@ -295,10 +295,14 @@ public class ColmiR0xDeviceSupport extends AbstractBTLEDeviceSupport {
if (daysAgo < 6) {
daysAgo++;
fetchHistoryHRV();
} else {
if (getDevice().getDeviceCoordinator().supportsTemperatureMeasurement()) {
fetchTemperature();
} else {
fetchRecordedDataFinished();
}
}
}
break;
case ColmiR0xConstants.CMD_FIND_DEVICE:
LOG.info("Received find device response: {}", StringUtils.bytesToHex(value));
@ -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);
}
}