Huawei: retrive and parse TruSleep data from new devices.

This commit is contained in:
Me7c7
2025-08-01 23:06:43 +02:00
committed by José Rebelo
parent f346de0cfb
commit 8bdb1b641e
17 changed files with 1103 additions and 111 deletions
@@ -58,7 +58,7 @@ public class GBDaoGenerator {
public static void main(String[] args) throws Exception {
final Schema schema = new Schema(107, MAIN_PACKAGE + ".entities");
final Schema schema = new Schema(108, MAIN_PACKAGE + ".entities");
Entity userAttributes = addUserAttributes(schema);
Entity user = addUserInfo(schema, userAttributes);
@@ -170,6 +170,8 @@ public class GBDaoGenerator {
addHuaweiActivitySample(schema, user, device);
addHuaweiStressSample(schema, user, device);
addHuaweiSleepStageSample(schema, user, device);
addHuaweiSleepStatsSample(schema, user, device);
addUltrahumanActivitySample(schema, user, device);
addUltrahumanDeviceStateSample(schema, user, device);
@@ -1548,6 +1550,36 @@ public class GBDaoGenerator {
return stressSample;
}
private static Entity addHuaweiSleepStageSample(Schema schema, Entity user, Entity device) {
Entity sleepStageSample = addEntity(schema, "HuaweiSleepStageSample");
addCommonTimeSampleProperties("AbstractTimeSample", sleepStageSample, user, device);
sleepStageSample.addIntProperty("stage").notNull();
return sleepStageSample;
}
private static Entity addHuaweiSleepStatsSample(Schema schema, Entity user, Entity device) {
Entity sample = addEntity(schema, "HuaweiSleepStatsSample");
sample.addImport(MAIN_PACKAGE + ".model.SleepScoreSample");
addCommonTimeSampleProperties("AbstractTimeSample", sample, user, device);
sample.implementsInterface("SleepScoreSample");
sample.addIntProperty("sleepScore").notNull().codeBeforeGetter(OVERRIDE);
sample.addLongProperty("bedTime").notNull();
sample.addLongProperty("risingTime").notNull();
sample.addLongProperty("wakeupTime").notNull();
sample.addIntProperty("sleepDataQuality").notNull();
sample.addIntProperty("deepPart").notNull();
sample.addIntProperty("snoreFreq").notNull();
sample.addIntProperty("sleepLatency").notNull();
sample.addIntProperty("sleepEfficiency").notNull();
sample.addIntProperty("minHeartRate").notNull();
sample.addIntProperty("maxHeartRate").notNull();
sample.addDoubleProperty("minOxygenSaturation").notNull();
sample.addDoubleProperty("maxOxygenSaturation").notNull();
sample.addDoubleProperty("minBreathRate").notNull();
sample.addDoubleProperty("maxBreathRate").notNull();
return sample;
}
private static Entity addHuaweiWorkoutSummarySample(Schema schema, Entity user, Entity device) {
Entity workoutSummary = addEntity(schema, "HuaweiWorkoutSummarySample");
@@ -40,6 +40,7 @@ import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser;
import nodomain.freeyourgadget.gadgetbridge.model.SleepScoreSample;
import nodomain.freeyourgadget.gadgetbridge.model.Spo2Sample;
import nodomain.freeyourgadget.gadgetbridge.model.StressSample;
import nodomain.freeyourgadget.gadgetbridge.model.TemperatureSample;
@@ -224,6 +225,16 @@ public abstract class HuaweiBRCoordinator extends AbstractBLClassicDeviceCoordin
return huaweiCoordinator.supportsFindDeviceAbility();
}
@Override
public boolean supportsRemSleep() {return huaweiCoordinator.getSupportsNewTrueSleep(device);}
@Override
public boolean supportsAwakeSleep() {return huaweiCoordinator.getSupportsNewTrueSleep(device);}
@Override
public boolean supportsSleepScore(final GBDevice device) { return huaweiCoordinator.getSupportsNewTrueSleep(device); }
@Override
public InstallHandler findInstallHandler(Uri uri, Context context) {
return huaweiCoordinator.getInstallHandler(uri, context);
@@ -254,6 +265,11 @@ public abstract class HuaweiBRCoordinator extends AbstractBLClassicDeviceCoordin
return new HuaweiStressSampleProvider(device, session);
}
@Override
public TimeSampleProvider<? extends SleepScoreSample> getSleepScoreProvider(final GBDevice device, final DaoSession session) {
return new HuaweiSleepStatsSampleProvider(device, session);
}
@Override
public int[] getStressRanges() {
return huaweiCoordinator.getStressRanges();
@@ -49,11 +49,12 @@ import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiActivitySampleDao;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiDictData;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiDictDataDao;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiDictDataValuesDao;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiSleepStageSampleDao;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiSleepStatsSampleDao;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiStressSampleDao;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutDataSampleDao;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutPaceSampleDao;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutSpO2SampleDao;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutSummaryAdditionalValuesSample;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutSummaryAdditionalValuesSampleDao;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutSummarySample;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutSummarySampleDao;
@@ -128,6 +129,12 @@ public class HuaweiCoordinator {
QueryBuilder<?> qb = session.getHuaweiActivitySampleDao().queryBuilder();
qb.where(HuaweiActivitySampleDao.Properties.DeviceId.eq(deviceId)).buildDelete().executeDeleteWithoutDetachingEntities();
QueryBuilder<?> sleepQb = session.getHuaweiSleepStageSampleDao().queryBuilder();
sleepQb.where(HuaweiSleepStageSampleDao.Properties.DeviceId.eq(deviceId)).buildDelete().executeDeleteWithoutDetachingEntities();
QueryBuilder<?> sleepStatsQb = session.getHuaweiSleepStatsSampleDao().queryBuilder();
sleepStatsQb.where(HuaweiSleepStatsSampleDao.Properties.DeviceId.eq(deviceId)).buildDelete().executeDeleteWithoutDetachingEntities();
QueryBuilder<?> stressQb = session.getHuaweiStressSampleDao().queryBuilder();
stressQb.where(HuaweiStressSampleDao.Properties.DeviceId.eq(deviceId)).buildDelete().executeDeleteWithoutDetachingEntities();
@@ -281,7 +288,7 @@ public class HuaweiCoordinator {
}
private int getNotificationConstraint(byte which) {
return (int)notificationConstraints.getShort(which);
return notificationConstraints.getShort(which);
}
public DeviceSpecificSettings getDeviceSpecificSettings(final GBDevice device) {
@@ -727,6 +734,18 @@ public class HuaweiCoordinator {
return false;
}
public boolean supportsDictSleepSync() {
if (supportsExpandCapability())
return supportsExpandCapability(143);
return false;
}
public boolean supportsBedTime() {
if (supportsExpandCapability())
return supportsExpandCapability(199);
return false;
}
public boolean supportsUnknownGender() {
if (supportsExpandCapability())
return supportsExpandCapability(0x57);
@@ -1086,4 +1105,8 @@ public class HuaweiCoordinator {
return new int[]{1800, 1800, 400};
}
public boolean getSupportsNewTrueSleep(final GBDevice device) {
return supportsTruSleep() && supportsDictSleepSync();
}
}
@@ -23,4 +23,6 @@ public class HuaweiDictTypes {
public static final int SKIN_TEMPERATURE_CLASS = 400012;
public static final int SKIN_TEMPERATURE_VALUE = 400012430;
public static final int SLEEP_DETAILS_CLASS = 700013;
}
@@ -41,6 +41,7 @@ import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser;
import nodomain.freeyourgadget.gadgetbridge.model.SleepScoreSample;
import nodomain.freeyourgadget.gadgetbridge.model.Spo2Sample;
import nodomain.freeyourgadget.gadgetbridge.model.StressSample;
import nodomain.freeyourgadget.gadgetbridge.model.TemperatureSample;
@@ -233,6 +234,15 @@ public abstract class HuaweiLECoordinator extends AbstractBLEDeviceCoordinator i
return huaweiCoordinator.supportsFindDeviceAbility();
}
@Override
public boolean supportsRemSleep() {return huaweiCoordinator.getSupportsNewTrueSleep(device);}
@Override
public boolean supportsAwakeSleep() {return huaweiCoordinator.getSupportsNewTrueSleep(device);}
@Override
public boolean supportsSleepScore(final GBDevice device) { return huaweiCoordinator.getSupportsNewTrueSleep(device); }
@Override
public InstallHandler findInstallHandler(Uri uri, Context context) {
return huaweiCoordinator.getInstallHandler(uri, context);
@@ -263,6 +273,11 @@ public abstract class HuaweiLECoordinator extends AbstractBLEDeviceCoordinator i
return new HuaweiStressSampleProvider(device, session);
}
@Override
public TimeSampleProvider<? extends SleepScoreSample> getSleepScoreProvider(final GBDevice device, final DaoSession session) {
return new HuaweiSleepStatsSampleProvider(device, session);
}
@Override
public int[] getStressRanges() {
return huaweiCoordinator.getStressRanges();
@@ -21,7 +21,9 @@ import androidx.annotation.Nullable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import de.greenrobot.dao.AbstractDao;
@@ -33,6 +35,8 @@ import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiActivitySample;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiActivitySampleDao;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiSleepStageSample;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiSleepStatsSample;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutDataSample;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutDataSampleDao;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutSummarySample;
@@ -42,6 +46,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
public class HuaweiSampleProvider extends AbstractSampleProvider<HuaweiActivitySample> {
/*
* We save all data by saving a marker at the begin and end. We do not actively use these for
* showing the data at the moment, but the samples are still saved as such, to keep the table
@@ -61,6 +66,9 @@ public class HuaweiSampleProvider extends AbstractSampleProvider<HuaweiActivityS
public static final int DEEP_SLEEP = 0x07;
public static final int LIGHT_SLEEP = 0x06;
public static final int TRUSLEEP_REM = 0x5656;
public static final int TRUSLEEP_AWAKE = 0x5658;
}
public HuaweiSampleProvider(GBDevice device, DaoSession session) {
@@ -69,26 +77,24 @@ public class HuaweiSampleProvider extends AbstractSampleProvider<HuaweiActivityS
@Override
public ActivityKind normalizeType(int rawType) {
switch (rawType) {
case RawTypes.DEEP_SLEEP:
return ActivityKind.DEEP_SLEEP;
case RawTypes.LIGHT_SLEEP:
return ActivityKind.LIGHT_SLEEP;
default:
return ActivityKind.UNKNOWN;
}
return switch (rawType) {
case RawTypes.DEEP_SLEEP -> ActivityKind.DEEP_SLEEP;
case RawTypes.LIGHT_SLEEP -> ActivityKind.LIGHT_SLEEP;
case RawTypes.TRUSLEEP_REM -> ActivityKind.REM_SLEEP;
case RawTypes.TRUSLEEP_AWAKE -> ActivityKind.AWAKE_SLEEP;
default -> ActivityKind.UNKNOWN;
};
}
@Override
public int toRawActivityKind(ActivityKind activityKind) {
switch (activityKind) {
case DEEP_SLEEP:
return RawTypes.DEEP_SLEEP;
case LIGHT_SLEEP:
return RawTypes.LIGHT_SLEEP;
default:
return RawTypes.NOT_MEASURED;
}
return switch (activityKind) {
case DEEP_SLEEP -> RawTypes.DEEP_SLEEP;
case LIGHT_SLEEP -> RawTypes.LIGHT_SLEEP;
case REM_SLEEP -> RawTypes.TRUSLEEP_REM;
case AWAKE_SLEEP -> RawTypes.TRUSLEEP_AWAKE;
default -> RawTypes.NOT_MEASURED;
};
}
@Override
@@ -292,6 +298,13 @@ public class HuaweiSampleProvider extends AbstractSampleProvider<HuaweiActivityS
return samples;
}
private static int adjustTimeToMinute(int time) {
if (time % 60 != 0) {
time = (time / 60) * 60;
}
return time;
}
/*
* This takes the following three steps:
* - Generate a sample every minute
@@ -302,18 +315,25 @@ public class HuaweiSampleProvider extends AbstractSampleProvider<HuaweiActivityS
protected List<HuaweiActivitySample> getGBActivitySamples(int timestamp_from, int timestamp_to) {
List<HuaweiActivitySample> processedSamples = new ArrayList<>();
timestamp_from = adjustTimeToMinute(timestamp_from);
timestamp_to = adjustTimeToMinute(timestamp_to);
for (int timestamp = timestamp_from; timestamp <= timestamp_to; timestamp += 60) {
processedSamples.add(createDummySample(timestamp));
}
overlayActivitySamples(processedSamples, timestamp_from, timestamp_to);
overlayWorkoutSamples(processedSamples, timestamp_from, timestamp_to);
overlaySleep(processedSamples, timestamp_from, timestamp_to);
return processedSamples;
}
@Override
protected List<HuaweiActivitySample> getGBActivitySamplesHighRes(int timestamp_from, int timestamp_to) {
timestamp_from = adjustTimeToMinute(timestamp_from);
timestamp_to = adjustTimeToMinute(timestamp_to);
List<HuaweiActivitySample> processedSamples = getRawOrderedActivitySamples(timestamp_from, timestamp_to);
addWorkoutSamples(processedSamples, timestamp_from, timestamp_to);
// Filter out the end markers before returning
@@ -544,4 +564,59 @@ public class HuaweiSampleProvider extends AbstractSampleProvider<HuaweiActivityS
newSample.setProvider(this);
return newSample;
}
private int toActivityKind(final HuaweiSleepStageSample stageSample) {
return switch (stageSample.getStage()) {
case 1 -> RawTypes.LIGHT_SLEEP;
case 2 -> RawTypes.TRUSLEEP_REM;
case 3 -> RawTypes.DEEP_SLEEP;
case 4 -> RawTypes.TRUSLEEP_AWAKE;
//case 5 -> RawTypes.UNKNOWN;
default -> RawTypes.UNKNOWN;
};
}
public void overlaySleep(final List<HuaweiActivitySample> samples, final int timestamp_from, final int timestamp_to) {
final Map<Long, Integer> stagesMap = new LinkedHashMap<>();
final HuaweiSleepStatsSampleProvider sleepStatsSampleProvider = new HuaweiSleepStatsSampleProvider(getDevice(), getSession());
final HuaweiSleepStageSampleProvider sleepStageSampleProvider = new HuaweiSleepStageSampleProvider(getDevice(), getSession());
List<HuaweiSleepStatsSample> sleepStatsSamples = sleepStatsSampleProvider.getSleepSamples(timestamp_from * 1000L, timestamp_to * 1000L);
if (!sleepStatsSamples.isEmpty()) {
for(HuaweiSleepStatsSample sample: sleepStatsSamples) {
final List<HuaweiSleepStageSample> stageSamples = sleepStageSampleProvider.getAllSamples(
sample.getTimestamp(),
sample.getWakeupTime() - 60000L
);
for (final HuaweiSleepStageSample stageSample : stageSamples) {
stagesMap.put(stageSample.getTimestamp(), toActivityKind(stageSample));
}
}
}
if (!stagesMap.isEmpty()) {
if (!samples.isEmpty()) {
for (final HuaweiActivitySample sample : samples) {
final long ts = sample.getTimestamp();
final Integer sleepType = stagesMap.get(ts * 1000L);
if (sleepType != null && !sleepType.equals(RawTypes.UNKNOWN)) {
sample.setRawKind(sleepType);
sample.setRawIntensity(ActivitySample.NOT_MEASURED);
}
}
} else {
for (int ts = timestamp_from; ts <= timestamp_to; ts += 60) {
final HuaweiActivitySample sample = createDummySample(ts);
final Integer sleepType = stagesMap.get(ts * 1000L);
if (sleepType != null && !sleepType.equals(RawTypes.UNKNOWN)) {
sample.setRawKind(sleepType);
sample.setRawIntensity(ActivitySample.NOT_MEASURED);
}
samples.add(sample);
}
}
}
}
}
@@ -0,0 +1,198 @@
package nodomain.freeyourgadget.gadgetbridge.devices.huawei;
import androidx.annotation.NonNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class HuaweiSequenceDataParser {
private static final Logger LOG = LoggerFactory.getLogger(HuaweiSequenceDataParser.class);
public static class SequenceData {
private int endTime;
private byte[] details;
private byte[] extraData;
private int startTime;
private int summaryType;
private int dataVersion; //??
private byte[] summary;
public int getStartTime() { return this.startTime;}
public void setStartTime(int startTime) {
this.startTime = startTime;
}
public int getEndTime() {
return this.endTime;
}
public void setEndTime(int endTime) {
this.endTime = endTime;
}
public int getDataVersion() { return this.dataVersion;}
public void setDataVersion(int version) { this.dataVersion = version;}
public int getSummaryType() { return this.summaryType;}
public void setSummaryType(int summaryType) {
this.summaryType = summaryType;
}
public void setExtraData(byte[] extraData) { this.extraData = extraData;}
public byte[] getSummary() { return this.summary;}
public void setSummary(byte[] summary) { this.summary = summary;}
public byte[] getDetails() {
return this.details;
}
public void setDetails(byte[] details) { this.details = details; }
@NonNull
@Override
public String toString() {
final StringBuilder sb = new StringBuilder("SequenceData{");
sb.append("startTime=").append(startTime);
sb.append(", endTime=").append(endTime);
sb.append(", version=").append(dataVersion);
sb.append(", summaryType=").append(summaryType);
sb.append(", summary=").append(Arrays.toString(summary));
sb.append(", details=").append(Arrays.toString(details));
sb.append(", extraData=").append(Arrays.toString(extraData));
sb.append('}');
return sb.toString();
}
}
public static class SequenceFileData {
private long dataSize;
private int fileType;
private byte[] extraData;
private int dictId;
private final List<SequenceData> sequenceDataList = new ArrayList<>();
private int fileVersion;
public void setDataSize(long dataSize) {
this.dataSize = dataSize;
}
public int getDictId() { return this.dictId; }
public void setDictId(int dictId) {
this.dictId = dictId;
}
public int getFileType() {
return this.fileType;
}
public void setFileType(int fileType) {
this.fileType = fileType;
}
public int getFileVersion() {
return this.fileVersion;
}
public void setFileVersion(int fileVersion) {
this.fileVersion = fileVersion;
}
public void setExtraData(byte[] extraData) { this.extraData = extraData;}
public List<SequenceData> getSequenceDataList() {
return this.sequenceDataList;
}
@NonNull
@Override
public String toString() {
final StringBuilder sb = new StringBuilder("SequenceFileData{");
sb.append("dataSize=").append(dataSize);
sb.append(", fileType=").append(fileType);
sb.append(", dictId=").append(dictId);
sb.append(", fileVersion=").append(fileVersion);
sb.append(", sequenceDataList=").append(sequenceDataList);
sb.append(", extraData=").append(Arrays.toString(extraData));
sb.append('}');
return sb.toString();
}
}
private static void parseSequenceData(ByteBuffer buffer, SequenceFileData fileData) {
while (buffer.remaining() >= 32) {
SequenceData data = new SequenceData();
data.setStartTime(buffer.getInt());
data.setEndTime(buffer.getInt());
data.setDataVersion(buffer.get());
data.setSummaryType(buffer.get());
int summaryDataLen = buffer.getShort();
int detailLen = buffer.getInt();
byte[] extraData = new byte[16];
buffer.get(extraData);
data.setExtraData(extraData);
// parseSummary
if (data.getDataVersion() == 1) {
if (buffer.remaining() < summaryDataLen) {
LOG.error("summaryDataLen {} exceeds remaining: {}", summaryDataLen, buffer.remaining());
} else {
byte[] bArr1 = new byte[summaryDataLen];
buffer.get(bArr1);
data.setSummary(bArr1);
}
}
// parse details
// TODO: In some my test files this value is too big. I suppose 20 Mb will be enough for now. Additional investigation required
if (detailLen <= 0 || detailLen > (20 * 1025 * 1024)) {
LOG.error("detailLen is invalid: {}", detailLen);
} else {
byte[] bArr2 = new byte[detailLen];
if (buffer.remaining() < detailLen) {
LOG.error("detailLen {} exceeds remaining: {}", detailLen, buffer.remaining());
}
buffer.get(bArr2);
data.setDetails(bArr2);
}
if (fileData.getSequenceDataList() != null) {
fileData.getSequenceDataList().add(data);
}
}
if (buffer.remaining() > 0) {
LOG.error("sequence data remaining: {}", buffer.remaining());
}
}
public static SequenceFileData parseSequenceFileData(byte[] data) {
if(data == null) {
LOG.error("Data is null");
return null;
}
if (data.length < 32) {
LOG.error("Invalid file length: {}", data.length);
return null;
}
ByteBuffer buffer = ByteBuffer.wrap(data);
buffer.order(ByteOrder.BIG_ENDIAN);
SequenceFileData fileData = new SequenceFileData();
fileData.setDataSize(buffer.getInt());
fileData.setDictId(buffer.getInt());
fileData.setFileType(buffer.getShort());
fileData.setFileVersion(buffer.getShort());
byte[] extraData = new byte[20];
buffer.get(extraData);
fileData.setExtraData(extraData);
parseSequenceData(buffer, fileData);
return fileData;
}
}
@@ -0,0 +1,40 @@
package nodomain.freeyourgadget.gadgetbridge.devices.huawei;
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.HuaweiSleepStageSample;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiSleepStageSampleDao;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
public class HuaweiSleepStageSampleProvider extends AbstractTimeSampleProvider<HuaweiSleepStageSample> {
public HuaweiSleepStageSampleProvider(final GBDevice device, final DaoSession session) {
super(device, session);
}
@NonNull
@Override
public AbstractDao<HuaweiSleepStageSample, ?> getSampleDao() {
return getSession().getHuaweiSleepStageSampleDao();
}
@NonNull
@Override
protected Property getTimestampSampleProperty() {
return HuaweiSleepStageSampleDao.Properties.Timestamp;
}
@NonNull
@Override
protected Property getDeviceIdentifierSampleProperty() {
return HuaweiSleepStageSampleDao.Properties.DeviceId;
}
@Override
public HuaweiSleepStageSample createSample() {
return new HuaweiSleepStageSample();
}
}
@@ -0,0 +1,90 @@
package nodomain.freeyourgadget.gadgetbridge.devices.huawei;
import androidx.annotation.NonNull;
import java.util.Collections;
import java.util.List;
import de.greenrobot.dao.AbstractDao;
import de.greenrobot.dao.Property;
import de.greenrobot.dao.query.QueryBuilder;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractTimeSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiActivitySample;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiActivitySampleDao;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiSleepStatsSample;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiSleepStatsSampleDao;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
public class HuaweiSleepStatsSampleProvider extends AbstractTimeSampleProvider<HuaweiSleepStatsSample> {
public HuaweiSleepStatsSampleProvider(final GBDevice device, final DaoSession session) {
super(device, session);
}
@NonNull
@Override
public AbstractDao<HuaweiSleepStatsSample, ?> getSampleDao() {
return getSession().getHuaweiSleepStatsSampleDao();
}
@NonNull
@Override
protected Property getTimestampSampleProperty() {
return HuaweiSleepStatsSampleDao.Properties.Timestamp;
}
@NonNull
@Override
protected Property getDeviceIdentifierSampleProperty() {
return HuaweiSleepStatsSampleDao.Properties.DeviceId;
}
@Override
public HuaweiSleepStatsSample createSample() {
return new HuaweiSleepStatsSample();
}
public long getLastSleepFetchTimestamp() {
QueryBuilder<HuaweiSleepStatsSample> qb = getSampleDao().queryBuilder();
Device dbDevice = DBHelper.findDevice(getDevice(), getSession());
if (dbDevice == null)
return 0;
final Property deviceProperty = HuaweiActivitySampleDao.Properties.DeviceId;
final Property timestampProperty = HuaweiActivitySampleDao.Properties.Timestamp;
qb.where(deviceProperty.eq(dbDevice.getId()))
.orderDesc(timestampProperty)
.limit(1);
List<HuaweiSleepStatsSample> samples = qb.build().list();
if (samples.isEmpty())
return 0;
HuaweiSleepStatsSample sample = samples.get(0);
return sample.getWakeupTime();
}
public List<HuaweiSleepStatsSample> getSleepSamples(final long timestampFrom, final long timestampTo) {
final QueryBuilder<HuaweiSleepStatsSample> qb = getSampleDao().queryBuilder();
final Property fallAsleepProperty = getTimestampSampleProperty();
final Property wakeupProperty = HuaweiSleepStatsSampleDao.Properties.WakeupTime;
final Device dbDevice = DBHelper.findDevice(getDevice(), getSession());
if (dbDevice == null) {
// no device, no samples
return Collections.emptyList();
}
final Property deviceProperty = getDeviceIdentifierSampleProperty();
qb.where(deviceProperty.eq(dbDevice.getId()),
qb.or(
qb.and(fallAsleepProperty.ge(timestampFrom), fallAsleepProperty.le(timestampTo)),
qb.and(wakeupProperty.ge(timestampFrom), wakeupProperty.le(timestampTo))
)
).orderAsc(fallAsleepProperty);
final List<HuaweiSleepStatsSample> samples = qb.build().list();
detachFromSession();
return samples;
}
}
@@ -322,6 +322,15 @@ public class HuaweiTLV {
return returnValue;
}
public List<HuaweiTLV> getAllContainerObjects() {
List<HuaweiTLV> returnValue = new ArrayList<>();
for (TLV tlv : valueMap) {
if (((tlv.getTag() & 0xFF) >>> 7) == 1)
returnValue.add(new HuaweiTLV().parse(tlv.getValue()));
}
return returnValue;
}
public boolean contains(int tag) {
for (TLV item : valueMap)
if (item.getTag() == (byte) tag)
@@ -0,0 +1,374 @@
package nodomain.freeyourgadget.gadgetbridge.devices.huawei;
import androidx.annotation.NonNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.List;
public class HuaweiTrueSleepSequenceDataParser {
private static final Logger LOG = LoggerFactory.getLogger(HuaweiTrueSleepSequenceDataParser.class);
public static class SleepStage {
private final long time;
private final int stage;
public SleepStage(long time, int stage) {
this.time = time;
this.stage = stage;
}
public int getStage() {
return stage;
}
public long getTime() {
return time;
}
@NonNull
@Override
public String toString() {
final StringBuffer sb = new StringBuffer("SleepStages{");
sb.append("time=").append(time);
sb.append(", stage=").append(stage);
sb.append('}');
return sb.toString();
}
}
public static class SleepSummary {
private long fallAsleepTime = -1;
private long bedTime = -1;
private long risingTime = -1;
private long wakeupTime = -1;
private int validData = -1;
private int sleepDataQuality = -1;
private int deepPart = -1;
private int snoreFreq = -1;
private int sleepScore = -1;
private int sleepLatency = -1;
private int sleepEfficiency = -1;
private int minHeartrate = -1;
private int maxHeartrate = -1;
private double minOxygenSaturation = -1;
private double maxOxygenSaturation = -1;
private double minBreathrate = -1;
private double maxBreathrate = -1;
public long getFallAsleepTime() { return fallAsleepTime; }
public void setFallAsleepTime(long fallAsleepTime) { this.fallAsleepTime = fallAsleepTime; }
public long getBedTime() { return bedTime; }
public void setBedTime(long bedTime) { this.bedTime = bedTime;}
public long getRisingTime() { return risingTime;}
public void setRisingTime(long risingTime) {this.risingTime = risingTime;}
public long getWakeupTime() {return wakeupTime;}
public void setWakeupTime(long wakeupTime) {this.wakeupTime = wakeupTime;}
public int getValidData() { return validData; }
public void setValidData(int validData) { this.validData = validData;}
public int getSleepDataQuality() { return sleepDataQuality;}
public void setSleepDataQuality(int sleepDataQuality) {this.sleepDataQuality = sleepDataQuality;}
public int getDeepPart() {return deepPart;}
public void setDeepPart(int deepPart) {this.deepPart = deepPart;}
public int getSnoreFreq() {return snoreFreq;}
public void setSnoreFreq(int snoreFreq) {this.snoreFreq = snoreFreq;}
public int getSleepScore() {return sleepScore;}
public void setSleepScore(int sleepScore) {this.sleepScore = sleepScore;}
public int getSleepLatency() {return sleepLatency;}
public void setSleepLatency(int sleepLatency) {this.sleepLatency = sleepLatency;}
public int getSleepEfficiency() {return sleepEfficiency;}
public void setSleepEfficiency(int sleepEfficiency) {this.sleepEfficiency = sleepEfficiency;}
public int getMinHeartrate() {return minHeartrate;}
public void setMinHeartrate(int minHeartrate) {this.minHeartrate = minHeartrate;}
public int getMaxHeartrate() {return maxHeartrate;}
public void setMaxHeartrate(int maxHeartrate) {this.maxHeartrate = maxHeartrate;}
public double getMinOxygenSaturation() {return minOxygenSaturation;}
public void setMinOxygenSaturation(double minOxygenSaturation) {this.minOxygenSaturation = minOxygenSaturation;}
public double getMaxOxygenSaturation() {return maxOxygenSaturation;}
public void setMaxOxygenSaturation(double maxOxygenSaturation) {this.maxOxygenSaturation = maxOxygenSaturation;}
public double getMinBreathrate() {return minBreathrate;}
public void setMinBreathrate(double minBreathrate) {this.minBreathrate = minBreathrate;}
public double getMaxBreathrate() {return maxBreathrate;}
public void setMaxBreathrate(double maxBreathrate) {this.maxBreathrate = maxBreathrate;}
@NonNull
@Override
public String toString() {
final StringBuilder sb = new StringBuilder("SleepDetails{");
sb.append("fallAsleepTime=").append(fallAsleepTime);
sb.append(", bedTime=").append(bedTime);
sb.append(", risingTime=").append(risingTime);
sb.append(", wakeupTime=").append(wakeupTime);
sb.append(", validData=").append(validData);
sb.append(", sleepDataQuality=").append(sleepDataQuality);
sb.append(", deepPart=").append(deepPart);
sb.append(", snoreFreq=").append(snoreFreq);
sb.append(", sleepScore=").append(sleepScore);
sb.append(", sleepLatency=").append(sleepLatency);
sb.append(", sleepEfficiency=").append(sleepEfficiency);
sb.append(", minHeartrate=").append(minHeartrate);
sb.append(", maxHeartrate=").append(maxHeartrate);
sb.append(", minOxygenSaturation=").append(minOxygenSaturation);
sb.append(", maxOxygenSaturation=").append(maxOxygenSaturation);
sb.append(", minBreathrate=").append(minBreathrate);
sb.append(", maxBreathrate=").append(maxBreathrate);
sb.append('}');
return sb.toString();
}
}
public static int readAsInteger(byte[] data, int def) {
if (data == null || data.length == 0 || data.length > 4) {
return def;
}
int res = 0;
for (int i = 0; i < data.length; i++) {
res |= (data[i] & 0xFF) << (((data.length - i) - 1) * 8);
}
return res;
}
public static long readAsLong(byte[] data, long def) {
if (data == null || data.length == 0 || data.length > 8) {
return def;
}
long res = 0;
for (int i = 0; i < data.length; i++) {
res |= (long) (data[i] & 0xFF) << (((data.length - i) - 1) * 8);
}
return res;
}
private static long getValueAsLong(int i, byte[] str2, long def) {
if (i == 1 || i == 2 || i == 4) { // int, byte, short (I suppose)
return readAsInteger(str2, (int) def);
} else if (i == 3) { // long
return readAsLong(str2, def);
} else if (i == 5) { // string
return def;
} else if (i == 6) { // double
return (long) Double.longBitsToDouble(readAsLong(str2, def));
} else {
return def;
}
}
private static double getValueAsDouble(int i, byte[] str2, double def) {
if (i == 1 || i == 2 || i == 4) { // int, byte, short (I suppose)
return readAsInteger(str2, (int) def);
} else if (i == 3) { // long
return readAsLong(str2, (int) def);
} else if (i == 5) { // string
return def;
} else if (i == 6) { // double
return Double.longBitsToDouble(readAsLong(str2, (int) def));
} else {
return def;
}
}
private static void fillSleepSummary(SleepSummary details, int dictId, int dataType, byte[] value) {
switch (dictId) {
case 700013686:
long fallAsleepTime = getValueAsLong(dataType, value, -1);
if(fallAsleepTime >=0)
details.setFallAsleepTime(fallAsleepTime);
break;
case 700013298:
long bedTime = getValueAsLong(dataType, value, -1);
if(bedTime >=0)
details.setBedTime(bedTime);
break;
case 700013973:
long risingTime = getValueAsLong(dataType, value, -1);
if(risingTime >=0)
details.setRisingTime(risingTime);
break;
case 700013156:
long wakeupTime = getValueAsLong(dataType, value, -1);
if(wakeupTime >=0)
details.setWakeupTime(wakeupTime);
break;
case 700013786:
long validData = getValueAsLong(dataType, value, -1);
if(validData >=0 && validData <= Integer.MAX_VALUE)
details.setValidData((int) validData);
break;
case 700013254:
long sleepDataQuality = getValueAsLong(dataType, value, -1);
if(sleepDataQuality >=0 && sleepDataQuality <= Integer.MAX_VALUE)
details.setSleepDataQuality((int) sleepDataQuality);
break;
case 700013679:
long deepPart = getValueAsLong(dataType, value, -1);
if(deepPart >=0 && deepPart <= Integer.MAX_VALUE)
details.setDeepPart((int) deepPart);
break;
case 700013721:
long snoreFreq = getValueAsLong(dataType, value, -1);
if(snoreFreq >=0 && snoreFreq <= Integer.MAX_VALUE)
details.setSnoreFreq((int) snoreFreq);
break;
case 700013245:
long sleepScore = getValueAsLong(dataType, value, -1);
if(sleepScore >=0 && sleepScore <= Integer.MAX_VALUE)
details.setSleepScore((int) sleepScore);
break;
case 700013713:
long sleepLatency = getValueAsLong(dataType, value, -1);
if(sleepLatency >=0 && sleepLatency <= Integer.MAX_VALUE)
details.setSleepLatency((int) sleepLatency);
break;
case 700013232:
long sleepEfficiency = getValueAsLong(dataType, value, -1);
if(sleepEfficiency >=0 && sleepEfficiency <= Integer.MAX_VALUE)
details.setSleepEfficiency((int) sleepEfficiency);
break;
case 700013436:
long minHeartrate = getValueAsLong(dataType, value, -1);
if(minHeartrate >= -1 && minHeartrate <= 255)
details.setMinHeartrate((int) minHeartrate);
break;
case 700013502:
long maxHeartrate = getValueAsLong(dataType, value, -1);
if(maxHeartrate >=0 && maxHeartrate <= 255)
details.setMaxHeartrate((int) maxHeartrate);
break;
case 700013340:
double minOxygenSaturation = getValueAsDouble(dataType, value, -1);
if(minOxygenSaturation >=0)
details.setMinOxygenSaturation((int) minOxygenSaturation);
break;
case 700013026:
double maxOxygenSaturation = getValueAsDouble(dataType, value, -1);
if(maxOxygenSaturation >=0)
details.setMaxOxygenSaturation((int) maxOxygenSaturation);
break;
case 700013646:
double minBreathrate = getValueAsDouble(dataType, value, -1);
if(minBreathrate >=0 && minBreathrate <= Integer.MAX_VALUE)
details.setMinBreathrate((int) minBreathrate);
break;
case 700013492:
double maxBreathrate = getValueAsDouble(dataType, value, -1);
if(maxBreathrate >=0 && maxBreathrate <= Integer.MAX_VALUE)
details.setMaxBreathrate((int) maxBreathrate);
break;
default:
LOG.info("Unknown dictId: {}", dictId);
}
}
private static SleepSummary parseTLVData(byte[] str) {
SleepSummary details = new SleepSummary();
HuaweiTLV tlv = new HuaweiTLV().parse(str);
List<HuaweiTLV> tlvs = tlv.getAllContainerObjects();
for (HuaweiTLV tv : tlvs) {
List<HuaweiTLV> tlvs2 = tv.getAllContainerObjects();
for (HuaweiTLV tv2 : tlvs2) {
try {
int dictId = tv2.getAsInteger(0x03);
int dataType = tv2.getAsInteger(0x04);
byte[] value = tv2.getBytes(0x5);
fillSleepSummary(details, dictId, dataType, value);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
return details;
}
public static SleepSummary parseSleepDataSummary(HuaweiSequenceDataParser.SequenceFileData fileData, HuaweiSequenceDataParser.SequenceData data) {
// summary type 2 is TLV. TLV works only for fileType != 2 (or maybe only for 1)
// currently only data version 1 supported
if (data != null && data.getDataVersion() == 1 && data.getSummaryType() == 2 && fileData.getFileType() != 2) {
return parseTLVData(data.getSummary());
}
return null;
}
private static long adjustTimeToMinute(long time) {
if (time % 60 != 0) {
time = (time / 60) * 60;
}
return time;
}
public static long getTime(long fallAsleepTime, long bedTime, int validData, boolean supportsBedTime) {
if (!supportsBedTime || validData == -1) {
return adjustTimeToMinute(fallAsleepTime);
}
return adjustTimeToMinute(bedTime);
}
public static List<SleepStage> parseSleepDetails(byte[] data, long time) {
if (data.length % 4 != 0) {
LOG.warn("detail length error");
return null;
}
List<SleepStage> result = new ArrayList<>();
ByteBuffer buffer = ByteBuffer.wrap(data);
buffer.order(ByteOrder.LITTLE_ENDIAN);
while (buffer.remaining() >= 4) {
int duration = buffer.getInt();
int status = buffer.get();
byte[] extra = new byte[3];
buffer.get(extra);
if (duration == 0 || status == 0 || duration % 60 != 0) {
LOG.error("invalid record duration: {} status: {} pos: {}", duration, status, buffer.position());
continue;
}
int minutes = duration / 60;
if (minutes > 1440) {
LOG.error("more than in one day: {} pos: {}", minutes, buffer.position());
continue;
}
for (int i = 0; i < minutes; i++) {
result.add(new SleepStage(time, status));
time += 60;
}
}
return result;
}
}
@@ -837,7 +837,7 @@ public class DeviceConfig {
case 0xf:
break;
case 0x11:
this.tlv.put(b, 1500012300); // Force AppVersion to 15.0.12.300
this.tlv.put(b, 1500106300); // Force AppVersion to 15.1.6.300
break;
case 0x15:
this.tlv.put(b); // Force buildOSPlatformVersion to ""
@@ -1857,7 +1857,7 @@ public class DeviceConfig {
this.commandId = id;
// Bits like ext capabilities
byte[] capabilities = {(byte) 0xFD, 0x17};
byte[] capabilities = {(byte) 0xFD, (byte) 0xF7, 0x53};
this.tlv = new HuaweiTLV()
.put(0x01, capabilities);
@@ -35,36 +35,29 @@ public class FileDownloadService2C {
SLEEP_DATA,
RRI,
GPS,
SEQUENCE_DATA,
UNKNOWN; // Never use this as input
static byte fileTypeToByte(FileType fileType) {
switch (fileType) {
case SLEEP_STATE:
return (byte) 0x0e;
case SLEEP_DATA:
return (byte) 0x0f;
case RRI:
return (byte) 0x10;
case GPS:
return (byte) 0x11;
default:
throw new RuntimeException();
}
return switch (fileType) {
case SLEEP_STATE -> (byte) 0x0e;
case SLEEP_DATA -> (byte) 0x0f;
case RRI -> (byte) 0x10;
case GPS -> (byte) 0x11;
case SEQUENCE_DATA -> (byte) 0x16;
default -> throw new RuntimeException();
};
}
static FileType byteToFileType(byte b) {
switch (b) {
case 0x0e:
return FileType.SLEEP_STATE;
case 0x0f:
return FileType.SLEEP_DATA;
case 0x10:
return FileType.RRI;
case 0x11:
return FileType.GPS;
default:
return FileType.UNKNOWN;
}
return switch (b) {
case 0x0e -> FileType.SLEEP_STATE;
case 0x0f -> FileType.SLEEP_DATA;
case 0x10 -> FileType.RRI;
case 0x11 -> FileType.GPS;
case 0x16 -> FileType.SEQUENCE_DATA;
default -> FileType.UNKNOWN;
};
}
}
@@ -72,19 +65,25 @@ public class FileDownloadService2C {
public static final int id = 0x01;
public static class Request extends HuaweiPacket {
public Request(ParamsProvider paramsProvider, String filename, FileType filetype, int startTime, int endTime) {
public Request(ParamsProvider paramsProvider, String filename, FileType filetype, int startTime, int endTime, int dictId) {
super(paramsProvider);
this.serviceId = FileDownloadService2C.id;
this.commandId = id;
byte fileId = FileType.fileTypeToByte(filetype);
// TODO: start and end time might be optional?
this.tlv = new HuaweiTLV()
.put(0x01, filename)
.put(0x02, FileType.fileTypeToByte(filetype))
.put(0x02, fileId)
.put(0x05, startTime)
.put(0x06, endTime);
if(dictId > 0 && fileId == 0x16) { // SEQUENCE_DATA
this.tlv.put(0x0c, dictId);
}
this.complete = true;
}
}
@@ -190,7 +189,7 @@ public class FileDownloadService2C {
public static class RequestBlock extends HuaweiPacket {
public static final int id = 0x04;
public RequestBlock(ParamsProvider paramsProvider, byte fileId, int offset, int size, boolean noEncrypt) {
public RequestBlock(ParamsProvider paramsProvider, byte fileId, int offset, int size, boolean noEncrypt, int dictId) {
super(paramsProvider);
this.serviceId = FileDownloadService2C.id;
@@ -201,6 +200,10 @@ public class FileDownloadService2C {
.put(0x02, offset)
.put(0x03, size);
if(dictId > 0 && fileId == 0x16) { // SEQUENCE_DATA
this.tlv.put(0x04, dictId);
}
this.complete = true;
this.isEncrypted = !noEncrypt;
}
@@ -123,6 +123,7 @@ public class HuaweiFileDownloadManager {
SLEEP_DATA,
RRI,
GPS,
SEQUENCE_DATA,
UNKNOWN // Never for input!
}
@@ -151,6 +152,8 @@ public class HuaweiFileDownloadManager {
private int startTime = 0;
private int endTime = 0;
private int dictId = 0;
// GPS type only
private short workoutId;
private Long databaseId;
@@ -180,7 +183,21 @@ public class HuaweiFileDownloadManager {
}
public static FileRequest rriFileRequest(boolean supportsRriNewSync, int startTime, int endTime, FileDownloadCallback fileDownloadCallback) {
return new FileRequest("rrisqi_data.bin", FileType.RRI, supportsRriNewSync, startTime, endTime, fileDownloadCallback);
return new FileRequest("rrisqi_data.bin", FileType.RRI, supportsRriNewSync, startTime, endTime,fileDownloadCallback);
}
private FileRequest(String filename, FileType fileType, int startTime, int endTime, int dictId, FileDownloadCallback fileDownloadCallback) {
this.filename = filename;
this.fileType = fileType;
this.newSync = true;
this.fileDownloadCallback = fileDownloadCallback;
this.startTime = startTime;
this.endTime = endTime;
this.dictId = dictId;
}
public static FileRequest sequenceDataFileRequest(int startTime, int endTime, int dictId, FileDownloadCallback fileDownloadCallback) {
return new FileRequest("sequence_data", FileType.SEQUENCE_DATA, startTime, endTime, dictId, fileDownloadCallback);
}
private FileRequest(String filename, FileType fileType, boolean newSync, FileDownloadCallback fileDownloadCallback) {
@@ -354,6 +371,10 @@ public class HuaweiFileDownloadManager {
public void setNeedVerify(boolean needVerify) {
this.needVerify = needVerify;
}
public int getDictId() { return dictId;}
public void setDictId(int dictId) { this.dictId = dictId;}
}
/**
@@ -71,9 +71,13 @@ import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiDictTypes;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiGpsParser;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiSequenceDataParser;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiSleepStageSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiSleepStatsSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiStressParser;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiStressSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiTruSleepParser;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiTrueSleepSequenceDataParser;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.CameraRemote;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.GpsAndTime;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Weather;
@@ -87,6 +91,8 @@ import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiDictData;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiDictDataDao;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiDictDataValues;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiDictDataValuesDao;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiSleepStageSample;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiSleepStatsSample;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiStressSample;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutDataSample;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutDataSampleDao;
@@ -924,7 +930,7 @@ public class HuaweiSupportProvider {
HuaweiP2PDataDictionarySyncService trackService = new HuaweiP2PDataDictionarySyncService(huaweiP2PManager);
trackService.register();
}
if(getHuaweiCoordinator().supportsNotificationsAddIconTimestamp()) {
if (getHuaweiCoordinator().supportsNotificationsAddIconTimestamp()) {
if (HuaweiP2PAppIcon.getRegisteredInstance(huaweiP2PManager) == null) {
HuaweiP2PAppIcon appIconService = new HuaweiP2PAppIcon(huaweiP2PManager);
appIconService.register();
@@ -932,11 +938,11 @@ public class HuaweiSupportProvider {
}
}
if(getHuaweiCoordinator().supportsThreeCircle() || getHuaweiCoordinator().supportsThreeCircleLite()) {
if (getHuaweiCoordinator().supportsThreeCircle() || getHuaweiCoordinator().supportsThreeCircleLite()) {
huaweiDataSyncTreeCircleGoals = new HuaweiDataSyncGoals(HuaweiSupportProvider.this);
}
if(getHuaweiCoordinator().supportsFindDeviceAbility()) {
if (getHuaweiCoordinator().supportsFindDeviceAbility()) {
huaweiDataSyncFindDevice = new HuaweiDataSyncFindDevice(HuaweiSupportProvider.this);
}
}
@@ -1231,9 +1237,9 @@ public class HuaweiSupportProvider {
}
public void setStepsGoal() {
if(huaweiDataSyncTreeCircleGoals != null) {
if (huaweiDataSyncTreeCircleGoals != null) {
int stepGoal = GBApplication.getPrefs().getInt(ActivityUser.PREF_USER_STEPS_GOAL, ActivityUser.defaultUserStepsGoal);
if(! huaweiDataSyncTreeCircleGoals.sendStepsGoal(stepGoal)) {
if (!huaweiDataSyncTreeCircleGoals.sendStepsGoal(stepGoal)) {
LOG.error("Error to set stand goal");
}
} else {
@@ -1246,9 +1252,9 @@ public class HuaweiSupportProvider {
}
public void setCaloriesBurntGoal() {
if(huaweiDataSyncTreeCircleGoals != null) {
if (huaweiDataSyncTreeCircleGoals != null) {
int caloriesBurntGoal = GBApplication.getPrefs().getInt(ActivityUser.PREF_USER_CALORIES_BURNT, ActivityUser.defaultUserCaloriesBurntGoal);
if(!huaweiDataSyncTreeCircleGoals.sendCaloriesBurntGoal(caloriesBurntGoal)) {
if (!huaweiDataSyncTreeCircleGoals.sendCaloriesBurntGoal(caloriesBurntGoal)) {
LOG.error("Error to set calories burnt goal");
}
} else {
@@ -1261,9 +1267,9 @@ public class HuaweiSupportProvider {
}
public void setFatBurnTime() {
if(huaweiDataSyncTreeCircleGoals != null) {
if (huaweiDataSyncTreeCircleGoals != null) {
int fatBurnTimeGoal = GBApplication.getPrefs().getInt(ActivityUser.PREF_USER_GOAL_FAT_BURN_TIME_MINUTES, ActivityUser.defaultUserFatBurnTimeMinutes);
if(!huaweiDataSyncTreeCircleGoals.sendExerciseGoal(fatBurnTimeGoal)) {
if (!huaweiDataSyncTreeCircleGoals.sendExerciseGoal(fatBurnTimeGoal)) {
LOG.error("Error to set exercise goal");
}
} else {
@@ -1276,9 +1282,9 @@ public class HuaweiSupportProvider {
}
public void setStandingTime() {
if(huaweiDataSyncTreeCircleGoals != null) {
if (huaweiDataSyncTreeCircleGoals != null) {
int standingTimeGoal = GBApplication.getPrefs().getInt(PREF_USER_GOAL_STANDING_TIME_HOURS, ActivityUser.defaultUserGoalStandingTimeHours);
if(!huaweiDataSyncTreeCircleGoals.sendStandGoal(standingTimeGoal)) {
if (!huaweiDataSyncTreeCircleGoals.sendStandGoal(standingTimeGoal)) {
LOG.error("Error to set stand goal");
}
} else {
@@ -1291,27 +1297,27 @@ public class HuaweiSupportProvider {
}
public void setActivityReminderStand() {
if(huaweiDataSyncTreeCircleGoals != null) {
if (huaweiDataSyncTreeCircleGoals != null) {
boolean state = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getBoolean(PREF_HUAWEI_ACTIVITY_REMINDER_STAND, true);
if(!huaweiDataSyncTreeCircleGoals.sendRemindersStand(state)) {
if (!huaweiDataSyncTreeCircleGoals.sendRemindersStand(state)) {
LOG.error("Error to set stand reminder");
}
}
}
public void setActivityReminderProgress() {
if(huaweiDataSyncTreeCircleGoals != null) {
if (huaweiDataSyncTreeCircleGoals != null) {
boolean state = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getBoolean(PREF_HUAWEI_ACTIVITY_REMINDER_PROGRESS, true);
if(!huaweiDataSyncTreeCircleGoals.sendRemindersProgress(state)) {
if (!huaweiDataSyncTreeCircleGoals.sendRemindersProgress(state)) {
LOG.error("Error to set progress reminder");
}
}
}
public void setActivityReminderGoalReached() {
if(huaweiDataSyncTreeCircleGoals != null) {
if (huaweiDataSyncTreeCircleGoals != null) {
boolean state = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getBoolean(PREF_HUAWEI_ACTIVITY_REMINDER_GOAL_REACHED, true);
if(!huaweiDataSyncTreeCircleGoals.sendRemindersGoalReached(state)) {
if (!huaweiDataSyncTreeCircleGoals.sendRemindersGoalReached(state)) {
LOG.error("Error to set goal reached reminder");
}
}
@@ -1325,7 +1331,7 @@ public class HuaweiSupportProvider {
}
LOG.debug("startUploadNotificationsAppIcons: {}", iconsToUpload);
HuaweiP2PAppIcon appIconService = HuaweiP2PAppIcon.getRegisteredInstance(this.huaweiP2PManager);
if(appIconService != null) {
if (appIconService != null) {
appIconService.addPackageName(new ArrayList<>(iconsToUpload));
}
}
@@ -1398,6 +1404,10 @@ public class HuaweiSupportProvider {
HuaweiSampleProvider sampleProvider = new HuaweiSampleProvider(gbDevice, db.getDaoSession());
sleepStart = sampleProvider.getLastSleepFetchTimestamp();
stepStart = sampleProvider.getLastStepFetchTimestamp();
HuaweiSleepStatsSampleProvider sleepStatsSampleProvider = new HuaweiSleepStatsSampleProvider(gbDevice, db.getDaoSession());
int sleepStart2 = (int) (sleepStatsSampleProvider.getLastSleepFetchTimestamp() / 1000L);
sleepStart = Math.max(sleepStart, sleepStart2);
} catch (Exception e) {
LOG.warn("Exception for getting start times, using 01/01/2000 - 00:00:00.");
}
@@ -1488,7 +1498,7 @@ public class HuaweiSupportProvider {
if (P2PSyncService != null) {
List<Integer> list = P2PSyncService.checkSupported(this.getHuaweiCoordinator(), Arrays.asList(HuaweiDictTypes.SKIN_TEMPERATURE_CLASS, HuaweiDictTypes.BLOOD_PRESSURE_CLASS));
if(!list.isEmpty()) {
if (!list.isEmpty()) {
syncState.setP2pSync(true);
P2PSyncService.startSync(list, complete -> {
LOG.info("Sync P2P Data complete");
@@ -1616,6 +1626,7 @@ public class HuaweiSupportProvider {
}
return msgId;
}
public void onNotification(NotificationSpec notificationSpec) {
if (!GBApplication.getDeviceSpecificSharedPrefs(getDevice().getAddress()).getBoolean(DeviceSettingsPreferenceConst.PREF_NOTIFICATION_ENABLE, false)) {
// Don't send notifications when they are disabled
@@ -2391,7 +2402,7 @@ public class HuaweiSupportProvider {
boolean automaticStressEnabled = GBApplication
.getDeviceSpecificSharedPrefs(getDevice().getAddress())
.getBoolean(HuaweiConstants.PREF_HUAWEI_STRESS_SWITCH, false);
if(automaticStressEnabled && getLastStressData() == null) {
if (automaticStressEnabled && getLastStressData() == null) {
SharedPreferences sharedPrefs = GBApplication.getDeviceSpecificSharedPrefs(this.getDevice().getAddress());
SharedPreferences.Editor editor = sharedPrefs.edit();
editor.putBoolean(HuaweiConstants.PREF_HUAWEI_STRESS_SWITCH, false);
@@ -2411,7 +2422,7 @@ public class HuaweiSupportProvider {
private void calibrateStress() {
if(stressCalibration != null) {
if (stressCalibration != null) {
GB.toast(context.getString(R.string.huawei_stress_calibrate_in_progress), Toast.LENGTH_SHORT, GB.INFO);
return;
}
@@ -2424,7 +2435,7 @@ public class HuaweiSupportProvider {
GB.toast(context.getString(R.string.huawei_stress_calibrate_done), Toast.LENGTH_SHORT, GB.INFO);
final Intent intent = new Intent(HuaweiStressCalibrationFragment.ACTION_STRESS_RESULT);
String str = HuaweiStressParser.stressDataToJsonStr(stressData);
if(!TextUtils.isEmpty(str)) {
if (!TextUtils.isEmpty(str)) {
intent.putExtra(HuaweiStressCalibrationFragment.EXTRA_STRESS_ERROR, false);
intent.putExtra(HuaweiStressCalibrationFragment.EXTRA_STRESS_DATA, str);
} else {
@@ -2449,7 +2460,7 @@ public class HuaweiSupportProvider {
GB.toast(context.getString(R.string.huawei_stress_calibrate_error), Toast.LENGTH_SHORT, GB.ERROR);
}
});
if(ret) {
if (ret) {
GB.toast(context.getString(R.string.huawei_stress_calibrate_started), Toast.LENGTH_SHORT, GB.INFO);
} else {
GB.toast(context.getString(R.string.huawei_stress_calibrate_in_progress), Toast.LENGTH_SHORT, GB.INFO);
@@ -2458,7 +2469,7 @@ public class HuaweiSupportProvider {
public void storeLastStressData(HuaweiStressParser.StressData data) {
String str = HuaweiStressParser.stressDataToJsonStr(data);
if(TextUtils.isEmpty(str)) {
if (TextUtils.isEmpty(str)) {
LOG.error("Failed to store stress data");
return;
}
@@ -2472,7 +2483,7 @@ public class HuaweiSupportProvider {
String str = GBApplication
.getDeviceSpecificSharedPrefs(this.getDevice().getAddress())
.getString(HuaweiConstants.PREF_HUAWEI_STRESS_LAST_DATA, "");
if(TextUtils.isEmpty(str)) {
if (TextUtils.isEmpty(str)) {
LOG.error("Failed to get saved stress data");
return null;
}
@@ -2546,7 +2557,7 @@ public class HuaweiSupportProvider {
LOG.info("enter onAppInstall uri: {}", uri);
HuaweiFwHelper huaweiFwHelper = new HuaweiFwHelper(uri, getContext());
if(huaweiFwHelper.isFirmware) {
if (huaweiFwHelper.isFirmware) {
huaweiOTAManager.startFwUpdate(huaweiFwHelper.fwInfo, uri);
return;
}
@@ -2785,11 +2796,97 @@ public class HuaweiSupportProvider {
}
private boolean downloadDictTrueSleepData(int start, int end) {
LOG.info("supportsDictSleepSync: {}", getHuaweiCoordinator().supportsDictSleepSync());
if (!getHuaweiCoordinator().supportsDictSleepSync()) {
return false;
}
huaweiFileDownloadManager.addToQueue(HuaweiFileDownloadManager.FileRequest.sequenceDataFileRequest(
start,
end,
HuaweiDictTypes.SLEEP_DETAILS_CLASS,
new HuaweiFileDownloadManager.FileDownloadCallback() {
@Override
public void downloadComplete(HuaweiFileDownloadManager.FileRequest fileRequest) {
HuaweiSequenceDataParser.SequenceFileData sequenceFileData = HuaweiSequenceDataParser.parseSequenceFileData(fileRequest.getData());
LOG.info("SLEEP File data: {}", sequenceFileData);
if (sequenceFileData != null) {
final List<HuaweiSleepStatsSample> sleepStatsSamples = new ArrayList<>();
final List<HuaweiSleepStageSample> sleepStageSamples = new ArrayList<>();
for (HuaweiSequenceDataParser.SequenceData sd : sequenceFileData.getSequenceDataList()) {
LOG.info("SLEEP SequenceData: {}", sd);
HuaweiTrueSleepSequenceDataParser.SleepSummary sleepDataSummary = HuaweiTrueSleepSequenceDataParser.parseSleepDataSummary(sequenceFileData, sd);
LOG.info("SLEEP DataSummary: {}", sleepDataSummary);
if(sleepDataSummary == null)
continue;
HuaweiSleepStatsSample sleepStat = new HuaweiSleepStatsSample();
sleepStat.setTimestamp(sleepDataSummary.getFallAsleepTime() * 1000L);
sleepStat.setSleepScore(sleepDataSummary.getSleepScore());
sleepStat.setBedTime(sleepDataSummary.getBedTime() * 1000L);
sleepStat.setRisingTime(sleepDataSummary.getRisingTime() * 1000L);
sleepStat.setWakeupTime(sleepDataSummary.getWakeupTime() * 1000L);
sleepStat.setSleepDataQuality(sleepDataSummary.getSleepDataQuality());
sleepStat.setDeepPart(sleepDataSummary.getDeepPart());
sleepStat.setSnoreFreq(sleepDataSummary.getSnoreFreq());
sleepStat.setSleepLatency(sleepDataSummary.getSleepLatency());
sleepStat.setSleepEfficiency(sleepDataSummary.getSleepEfficiency());
sleepStat.setMinHeartRate(sleepDataSummary.getMinHeartrate());
sleepStat.setMaxHeartRate(sleepDataSummary.getMaxHeartrate());
sleepStat.setMinOxygenSaturation(sleepDataSummary.getMinOxygenSaturation());
sleepStat.setMaxOxygenSaturation(sleepDataSummary.getMaxOxygenSaturation());
sleepStat.setMinBreathRate(sleepDataSummary.getMinBreathrate());
sleepStat.setMaxBreathRate(sleepDataSummary.getMaxBreathrate());
sleepStatsSamples.add(sleepStat);
long time = HuaweiTrueSleepSequenceDataParser.getTime(sleepDataSummary.getFallAsleepTime(), sleepDataSummary.getBedTime(), sleepDataSummary.getValidData(), getHuaweiCoordinator().supportsBedTime());
LOG.info("SLEEP Time: {}", time);
List<HuaweiTrueSleepSequenceDataParser.SleepStage> stages = HuaweiTrueSleepSequenceDataParser.parseSleepDetails(sd.getDetails(), time);
LOG.info("SLEEP Stages: {}", stages);
if(stages != null) {
for (HuaweiTrueSleepSequenceDataParser.SleepStage st : stages) {
HuaweiSleepStageSample sample = new HuaweiSleepStageSample();
sample.setTimestamp(st.getTime() * 1000L);
sample.setStage(st.getStage());
sleepStageSamples.add(sample);
}
}
}
try (DBHandler db = GBApplication.acquireDB()) {
final DaoSession session = db.getDaoSession();
new HuaweiSleepStatsSampleProvider(gbDevice, session).persistForDevice(context, gbDevice, sleepStatsSamples);
new HuaweiSleepStageSampleProvider(gbDevice, session).persistForDevice(context, gbDevice, sleepStageSamples);
} catch (Exception e) {
LOG.error("Cannot save sleep, continue");
}
}
syncState.setActivitySync(false);
}
@Override
public void downloadException(HuaweiFileDownloadManager.HuaweiFileDownloadException e) {
super.downloadException(e);
syncState.setActivitySync(false);
}
}
), true);
return true;
}
public boolean downloadTruSleepData(int start, int end) {
// We only get the data if TruSleep is supported
if (!getHuaweiCoordinator().supportsTruSleep())
return false;
if (downloadDictTrueSleepData(start, end))
return true;
HuaweiTruSleepParser.SleepFileDownloadCallback callback = new HuaweiTruSleepParser.SleepFileDownloadCallback(this) {
@Override
public void syncComplete(byte[] statusData, byte[] sleepData) {
@@ -2845,14 +2942,14 @@ public class HuaweiSupportProvider {
LOG.debug("Parsing stress file");
HuaweiStressParser.RriFileData results = HuaweiStressParser.parseRri(fileRequest.getData());
LOG.info("stress result: {}", results);
if(results != null && !results.stressData.isEmpty()) {
if (results != null && !results.stressData.isEmpty()) {
HuaweiStressParser.StressData stressData = results.stressData.get(results.stressData.size() - 1);
LOG.info("Last stored stress data: {}", stressData);
HuaweiStressParser.StressData currentStressData = getLastStressData();
if(currentStressData == null || stressData.endTime > currentStressData.endTime) {
if (currentStressData == null || stressData.endTime > currentStressData.endTime) {
storeLastStressData(stressData);
}
for(HuaweiStressParser.StressData dt: results.stressData) {
for (HuaweiStressParser.StressData dt : results.stressData) {
addStressData(dt.startTime, dt.endTime, dt.score, dt.level);
}
}
@@ -3024,6 +3121,10 @@ public class HuaweiSupportProvider {
public void onTestNewFunction() {
gbDevice.setBusyTask(R.string.busy_task_downloading, getContext());
gbDevice.sendDeviceUpdateIntent(getContext());
downloadDictTrueSleepData(0, (int) (System.currentTimeMillis() / 1000));
}
public void onMusicListReq() {
@@ -3040,13 +3141,13 @@ public class HuaweiSupportProvider {
return;
}
if(cannedMessagesSpec.cannedMessages.length == 0) {
if (cannedMessagesSpec.cannedMessages.length == 0) {
GB.toast(context, HuaweiSupportProvider.this.getContext().getString(R.string.canned_replies_not_empty), Toast.LENGTH_SHORT, GB.WARN);
LOG.warn(HuaweiSupportProvider.this.getContext().getString(R.string.canned_replies_not_empty));
}
HuaweiP2PCannedRepliesService cannedRepliesService = HuaweiP2PCannedRepliesService.getRegisteredInstance(huaweiP2PManager);
if(cannedRepliesService == null) {
if (cannedRepliesService == null) {
LOG.warn("P2P canned replies service is not registered");
return;
}
@@ -3054,7 +3155,7 @@ public class HuaweiSupportProvider {
}
public void onFindDevice(boolean start) {
if(huaweiDataSyncFindDevice != null) {
if (huaweiDataSyncFindDevice != null) {
if (start) {
huaweiDataSyncFindDevice.sendStartFindDevice();
} else {
@@ -48,7 +48,8 @@ public class GetFileBlockRequest extends Request {
this.request.getFileId(),
this.request.getCurrentOffset(),
this.request.getCurrentBlockSize(),
this.request.isNoEncrypt()
this.request.isNoEncrypt(),
this.request.getDictId()
).serialize();
else
return new FileDownloadService0A.RequestBlock.Request(
@@ -52,33 +52,25 @@ public class GetFileDownloadInitRequest extends Request {
}
private FileDownloadService2C.FileType convertFileTypeTo2C(HuaweiFileDownloadManager.FileType type) {
switch (type) {
case SLEEP_STATE:
return FileDownloadService2C.FileType.SLEEP_STATE;
case SLEEP_DATA:
return FileDownloadService2C.FileType.SLEEP_DATA;
case RRI:
return FileDownloadService2C.FileType.RRI;
case GPS:
return FileDownloadService2C.FileType.GPS;
default:
return FileDownloadService2C.FileType.UNKNOWN;
}
return switch (type) {
case SLEEP_STATE -> FileDownloadService2C.FileType.SLEEP_STATE;
case SLEEP_DATA -> FileDownloadService2C.FileType.SLEEP_DATA;
case RRI -> FileDownloadService2C.FileType.RRI;
case GPS -> FileDownloadService2C.FileType.GPS;
case SEQUENCE_DATA -> FileDownloadService2C.FileType.SEQUENCE_DATA;
default -> FileDownloadService2C.FileType.UNKNOWN;
};
}
private HuaweiFileDownloadManager.FileType convertFileTypeFrom2C(FileDownloadService2C.FileType type) {
switch (type) {
case SLEEP_STATE:
return HuaweiFileDownloadManager.FileType.SLEEP_STATE;
case SLEEP_DATA:
return HuaweiFileDownloadManager.FileType.SLEEP_DATA;
case RRI:
return HuaweiFileDownloadManager.FileType.RRI;
case GPS:
return HuaweiFileDownloadManager.FileType.GPS;
default:
return HuaweiFileDownloadManager.FileType.UNKNOWN;
}
return switch (type) {
case SLEEP_STATE -> HuaweiFileDownloadManager.FileType.SLEEP_STATE;
case SLEEP_DATA -> HuaweiFileDownloadManager.FileType.SLEEP_DATA;
case RRI -> HuaweiFileDownloadManager.FileType.RRI;
case GPS -> HuaweiFileDownloadManager.FileType.GPS;
case SEQUENCE_DATA -> HuaweiFileDownloadManager.FileType.SEQUENCE_DATA;
default -> HuaweiFileDownloadManager.FileType.UNKNOWN;
};
}
@Override
@@ -88,7 +80,7 @@ public class GetFileDownloadInitRequest extends Request {
FileDownloadService2C.FileType type = convertFileTypeTo2C(request.getFileType());
if (type == FileDownloadService2C.FileType.UNKNOWN)
throw new RequestCreationException("Cannot convert type " + request.getFileType());
return new FileDownloadService2C.FileDownloadInit.Request(paramsProvider, request.getFilename(), type, request.getStartTime(), request.getEndTime()).serialize();
return new FileDownloadService2C.FileDownloadInit.Request(paramsProvider, request.getFilename(), type, request.getStartTime(), request.getEndTime(), request.getDictId()).serialize();
} else {
if (this.request.getFileType() == HuaweiFileDownloadManager.FileType.DEBUG)
return new FileDownloadService0A.FileDownloadInit.DebugFilesRequest(paramsProvider).serialize();