From c5d70c1df82b2244c2537662420d7b469a774d63 Mon Sep 17 00:00:00 2001 From: Me7c7 Date: Tue, 25 Nov 2025 11:46:08 +0200 Subject: [PATCH] Huawei: sync sleep APNEA data. No UI --- .../gadgetbridge/daogen/GBDaoGenerator.java | 11 +- .../devices/huawei/HuaweiDictTypes.java | 4 +- .../HuaweiSleepApneaSampleProvider.java | 66 +++++++++ .../huawei/packets/AccountRelated.java | 138 +++++++++--------- .../devices/huawei/HuaweiSupportProvider.java | 130 ++++++++++++----- .../HuaweiP2PDataDictionarySyncService.java | 2 + 6 files changed, 245 insertions(+), 106 deletions(-) create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiSleepApneaSampleProvider.java diff --git a/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java b/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java index f04106e84f..99876b26fd 100644 --- a/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java +++ b/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java @@ -56,7 +56,7 @@ public class GBDaoGenerator { private static final String TIMESTAMP_TO = "timestampTo"; public static void main(String[] args) throws Exception { - final Schema schema = new Schema(119, MAIN_PACKAGE + ".entities"); + final Schema schema = new Schema(120, MAIN_PACKAGE + ".entities"); Entity userAttributes = addUserAttributes(schema); Entity user = addUserInfo(schema, userAttributes); @@ -171,6 +171,7 @@ public class GBDaoGenerator { addHuaweiStressSample(schema, user, device); addHuaweiSleepStageSample(schema, user, device); addHuaweiSleepStatsSample(schema, user, device); + addHuaweiSleepApneaSample(schema, user, device); addHuaweiTemperatureSample(schema, user, device); addHuaweiHrvValuesSample(schema, user, device); addHuaweiEmotionsSample(schema, user, device); @@ -1580,6 +1581,14 @@ public class GBDaoGenerator { return sleepStageSample; } + private static Entity addHuaweiSleepApneaSample(Schema schema, Entity user, Entity device) { + Entity sleepApneaSample = addEntity(schema, "HuaweiSleepApneaSample"); + addCommonTimeSampleProperties("AbstractTimeSample", sleepApneaSample, user, device); + sleepApneaSample.addLongProperty("lastTimestamp").notNull().index(); + sleepApneaSample.addIntProperty("level").notNull(); + return sleepApneaSample; + } + private static Entity addHuaweiSleepStatsSample(Schema schema, Entity user, Entity device) { Entity sample = addEntity(schema, "HuaweiSleepStatsSample"); addCommonTimeSampleProperties("AbstractTimeSample", sample, user, device); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiDictTypes.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiDictTypes.java index 3fa0881080..6b21afde20 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiDictTypes.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiDictTypes.java @@ -32,10 +32,12 @@ public class HuaweiDictTypes { public static final int HRV_RMSSD_VALUE = 500044831; public static final int EMOTION_CLASS = 500031; - public static final int EMOTION_STATUS_VALUE = 500031195; public static final int EMOTION_VALENCE_CHARACTER_VALUE = 500031347; public static final int EMOTION_ORIGIN_STATUS_VALUE = 500031160; public static final int EMOTION_AROUSAL_CHARACTER_VALUE = 500031842; + public static final int SLEEP_APNEA_CLASS = 500002; + public static final int SLEEP_APNEA_LEVEL_VALUE = 500002847; + } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiSleepApneaSampleProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiSleepApneaSampleProvider.java new file mode 100644 index 0000000000..ce39c67b3f --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiSleepApneaSampleProvider.java @@ -0,0 +1,66 @@ +package nodomain.freeyourgadget.gadgetbridge.devices.huawei; + +import androidx.annotation.NonNull; + +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.HuaweiSleepApneaSample; +import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiSleepApneaSampleDao; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; + +public class HuaweiSleepApneaSampleProvider extends AbstractTimeSampleProvider { + public HuaweiSleepApneaSampleProvider(final GBDevice device, final DaoSession session) { + super(device, session); + } + + @NonNull + @Override + public AbstractDao getSampleDao() { + return getSession().getHuaweiSleepApneaSampleDao(); + } + + @NonNull + @Override + protected Property getTimestampSampleProperty() { + return HuaweiSleepApneaSampleDao.Properties.Timestamp; + } + + @NonNull + @Override + protected Property getDeviceIdentifierSampleProperty() { + return HuaweiSleepApneaSampleDao.Properties.DeviceId; + } + + @Override + public HuaweiSleepApneaSample createSample() { + return new HuaweiSleepApneaSample(); + } + + + public long getLastFetchTimestamp() { + QueryBuilder qb = getSampleDao().queryBuilder(); + Device dbDevice = DBHelper.findDevice(getDevice(), getSession()); + if (dbDevice == null) + return 0; + final Property deviceProperty = HuaweiSleepApneaSampleDao.Properties.DeviceId; + final Property timestampProperty = HuaweiSleepApneaSampleDao.Properties.LastTimestamp; + + qb.where(deviceProperty.eq(dbDevice.getId())) + .orderDesc(timestampProperty) + .limit(1); + + List samples = qb.build().list(); + if (samples.isEmpty()) + return 0; + + HuaweiSleepApneaSample sample = samples.get(0); + return sample.getLastTimestamp(); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/AccountRelated.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/AccountRelated.java index c27e4c2500..04eed59026 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/AccountRelated.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/AccountRelated.java @@ -20,90 +20,90 @@ import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket; import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiTLV; public class AccountRelated { - public static final byte id = 0x1A; + public static final byte id = 0x1A; - public static class SendAccountToDevice { - public static final byte id = 0x01; + public static class SendAccountToDevice { + public static final byte id = 0x01; - public static class Request extends HuaweiPacket { - public Request (ParamsProvider paramsProvider, String account) { - super(paramsProvider); + public static class Request extends HuaweiPacket { + public Request(ParamsProvider paramsProvider, String account) { + super(paramsProvider); - this.serviceId = AccountRelated.id; - this.commandId = id; + this.serviceId = AccountRelated.id; + this.commandId = id; - this.tlv = new HuaweiTLV(); - if (account.length() > 0) { - tlv.put(0x01, account); - } else { - tlv.put(0x01); - } - this.complete = true; - } - } + this.tlv = new HuaweiTLV(); + if (!account.isEmpty()) { + tlv.put(0x01, account); + } else { + tlv.put(0x01); + } + this.complete = true; + } + } - public static class Response extends HuaweiPacket { - public Response (ParamsProvider paramsProvider) { - super(paramsProvider); - } - } - } + public static class Response extends HuaweiPacket { + public Response(ParamsProvider paramsProvider) { + super(paramsProvider); + } + } + } - public static class SendExtendedAccountToDevice { - public static final byte id = 0x05; + public static class SendExtendedAccountToDevice { + public static final byte id = 0x05; - public static class Request extends HuaweiPacket { - public Request (ParamsProvider paramsProvider, boolean accountPairingOptimization, String account) { - super(paramsProvider); + public static class Request extends HuaweiPacket { + public Request(ParamsProvider paramsProvider, boolean accountPairingOptimization, String account) { + super(paramsProvider); - this.serviceId = AccountRelated.id; - this.commandId = id; + this.serviceId = AccountRelated.id; + this.commandId = id; - this.tlv = new HuaweiTLV(); - if (account.length() > 0) { - tlv.put(0x01, account); - } else { - tlv.put(0x01, (byte)0x00); - } + this.tlv = new HuaweiTLV(); + if (!account.isEmpty()) { + tlv.put(0x01, account); + } else { + tlv.put(0x01, (byte) 0x00); + } - if (accountPairingOptimization) { - this.tlv.put(0x03, (byte)0x01); - } - this.complete = true; - } - } + if (accountPairingOptimization) { + this.tlv.put(0x03, (byte) 0x01); + } + this.complete = true; + } + } - public static class Response extends HuaweiPacket { - public Response (ParamsProvider paramsProvider) { - super(paramsProvider); - } - } - } + public static class Response extends HuaweiPacket { + public Response(ParamsProvider paramsProvider) { + super(paramsProvider); + } + } + } - public static class SendCountryCodeToDevice { - public static final byte id = 0x0a; + public static class SendCountryCodeToDevice { + public static final byte id = 0x0a; - public static class Request extends HuaweiPacket { - public Request (ParamsProvider paramsProvider, String countryCode, Byte siteId) { - super(paramsProvider); + public static class Request extends HuaweiPacket { + public Request(ParamsProvider paramsProvider, String countryCode, Byte siteId) { + super(paramsProvider); - this.serviceId = AccountRelated.id; - this.commandId = id; + this.serviceId = AccountRelated.id; + this.commandId = id; - this.tlv = new HuaweiTLV(); + this.tlv = new HuaweiTLV(); - tlv.put(0x01, countryCode); - if(siteId != null) { - tlv.put(0x02, siteId); - } - this.complete = true; - } - } + tlv.put(0x01, countryCode); + if (siteId != null) { + tlv.put(0x02, siteId); + } + this.complete = true; + } + } - public static class Response extends HuaweiPacket { - public Response (ParamsProvider paramsProvider) { - super(paramsProvider); - } - } - } + public static class Response extends HuaweiPacket { + public Response(ParamsProvider paramsProvider) { + super(paramsProvider); + } + } + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiSupportProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiSupportProvider.java index d94bab65cb..6ad541b533 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiSupportProvider.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiSupportProvider.java @@ -44,6 +44,7 @@ import java.nio.charset.StandardCharsets; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; +import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -77,6 +78,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiHrvValueSampleP import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket; import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiSampleProvider; import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiSequenceDataFileParser; +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiSleepApneaSampleProvider; import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiSleepStageSampleProvider; import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiSleepStatsSampleProvider; import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiStressParser; @@ -97,6 +99,7 @@ import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummaryDao; import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiActivitySample; import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiEmotionsSample; import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiHrvValueSample; +import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiSleepApneaSample; import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiSleepStageSample; import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiSleepStatsSample; import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiStressSample; @@ -1578,7 +1581,11 @@ public class HuaweiSupportProvider { HuaweiP2PDataDictionarySyncService P2PSyncService = HuaweiP2PDataDictionarySyncService.getRegisteredInstance(huaweiP2PManager); if (P2PSyncService != null) { - List list = P2PSyncService.checkSupported(this.getHuaweiCoordinator(), Arrays.asList(HuaweiDictTypes.SKIN_TEMPERATURE_CLASS, HuaweiDictTypes.HRV_CLASS, HuaweiDictTypes.EMOTION_CLASS)); + List list = P2PSyncService.checkSupported(this.getHuaweiCoordinator(), Arrays.asList( + HuaweiDictTypes.SKIN_TEMPERATURE_CLASS, + HuaweiDictTypes.HRV_CLASS, + HuaweiDictTypes.EMOTION_CLASS, + HuaweiDictTypes.SLEEP_APNEA_CLASS)); if (!list.isEmpty()) { syncState.setP2pSync(true); P2PSyncService.startSync(list, new HuaweiP2PDataDictionarySyncService.DictionarySyncCallback() { @@ -1590,7 +1597,7 @@ public class HuaweiSupportProvider { HuaweiTemperatureSampleProvider sleepStatsSampleProvider = new HuaweiTemperatureSampleProvider(gbDevice, db.getDaoSession()); return sleepStatsSampleProvider.getLastFetchTimestamp(); } catch (Exception e) { - LOG.warn("Exception for getting temperature start time", e); + LOG.warn("Exception for getting skin temperature start time", e); } return 0; } else if (dictClass == HuaweiDictTypes.HRV_CLASS) { @@ -1606,9 +1613,11 @@ public class HuaweiSupportProvider { HuaweiEmotionsSampleProvider emotionsStatsSampleProvider = new HuaweiEmotionsSampleProvider(gbDevice, db.getDaoSession()); return emotionsStatsSampleProvider.getLastFetchTimestamp(); } catch (Exception e) { - LOG.warn("Exception for getting HRV start time", e); + LOG.warn("Exception for getting emotion start time", e); } return 0; + } else if (dictClass == HuaweiDictTypes.SLEEP_APNEA_CLASS) { + return 0; } return -1; } @@ -1621,19 +1630,29 @@ public class HuaweiSupportProvider { for (HuaweiP2PDataDictionarySyncService.DictData dt : dictData) { long timestamp = dt.getStartTimestamp(); long lastTime = Math.max(dt.getEndTimestamp(), dt.getModifyTimestamp()); + Float temperature = null; for (HuaweiP2PDataDictionarySyncService.DictData.DictDataValue val : dt.getData()) { - if (val.getTag() == 10 && val.getDataType() == HuaweiDictTypes.SKIN_TEMPERATURE_VALUE) { - double skinTemperature = HuaweiUtil.convBytes2Double(val.getValue()); - if (skinTemperature >= 20 && skinTemperature <= 42) { - HuaweiTemperatureSample sample = new HuaweiTemperatureSample(); - sample.setTimestamp(timestamp); - sample.setLastTimestamp(lastTime); - sample.setTemperature((float) skinTemperature); - sample.setTemperatureType(TemperatureSample.TYPE_SKIN); - sample.setTemperatureLocation(TemperatureSample.LOCATION_WRIST); - temperatureSamples.add(sample); - } + if (val.getTag() != 10) { + LOG.info("skin temperature unexpected tag: {}", val.getTag()); + continue; } + if (val.getDataType() == HuaweiDictTypes.SKIN_TEMPERATURE_VALUE) { + double value = HuaweiUtil.convBytes2Double(val.getValue()); + if (value >= 20 && value <= 42) { + temperature = (float) value; + } + } else { + LOG.info("skin temperature unknown data type: {}", val.getDataType()); + } + } + if(temperature != null) { + HuaweiTemperatureSample sample = new HuaweiTemperatureSample(); + sample.setTimestamp(timestamp); + sample.setLastTimestamp(lastTime); + sample.setTemperature(temperature); + sample.setTemperatureType(TemperatureSample.TYPE_SKIN); + sample.setTemperatureLocation(TemperatureSample.LOCATION_WRIST); + temperatureSamples.add(sample); } } try (DBHandler db = GBApplication.acquireDB()) { @@ -1647,25 +1666,34 @@ public class HuaweiSupportProvider { for (HuaweiP2PDataDictionarySyncService.DictData dt : dictData) { long timestamp = dt.getStartTimestamp(); long lastTime = Math.max(dt.getEndTimestamp(), dt.getModifyTimestamp()); + Integer rmssd = null; for (HuaweiP2PDataDictionarySyncService.DictData.DictDataValue val : dt.getData()) { - if (val.getTag() == 10 && val.getDataType() == HuaweiDictTypes.HRV_RMSSD_VALUE) { - double rmssd = HuaweiUtil.convBytes2Double(val.getValue()); - int value = (int) rmssd; - if (value >= 0 && value <= 200) { - HuaweiHrvValueSample sample = new HuaweiHrvValueSample(); - sample.setTimestamp(timestamp); - sample.setLastTimestamp(lastTime); - sample.setValue(value); - hrvSamples.add(sample); - } + if (val.getTag() != 10) { + LOG.info("HRV unexpected tag: {}", val.getTag()); + continue; } + if (val.getDataType() == HuaweiDictTypes.HRV_RMSSD_VALUE) { + double value = HuaweiUtil.convBytes2Double(val.getValue()); + if (value >= 0 && value <= 200) { + rmssd = (int) value; + } + } else { + LOG.info("HRV unknown data type: {}", val.getDataType()); + } + } + if(rmssd != null) { + HuaweiHrvValueSample sample = new HuaweiHrvValueSample(); + sample.setTimestamp(timestamp); + sample.setLastTimestamp(lastTime); + sample.setValue(rmssd); + hrvSamples.add(sample); } } try (DBHandler db = GBApplication.acquireDB()) { final DaoSession session = db.getDaoSession(); new HuaweiHrvValueSampleProvider(gbDevice, session).persistForDevice(context, gbDevice, hrvSamples); } catch (Exception e) { - LOG.error("Cannot save skin HRV samples, continue"); + LOG.error("Cannot save HRV samples, continue"); } } else if (dictClass == HuaweiDictTypes.EMOTION_CLASS) { List emotionsSamples = new ArrayList<>(); @@ -1704,7 +1732,6 @@ public class HuaweiSupportProvider { } else { LOG.info("emotions unknown data type: {}", val.getDataType()); } - } if (status != null || valenceCharacter != null || originStatus != null || arousalCharacter != null) { HuaweiEmotionsSample sample = new HuaweiEmotionsSample(); @@ -1716,13 +1743,49 @@ public class HuaweiSupportProvider { sample.setArousalCharacter(arousalCharacter); emotionsSamples.add(sample); } - } try (DBHandler db = GBApplication.acquireDB()) { final DaoSession session = db.getDaoSession(); new HuaweiEmotionsSampleProvider(gbDevice, session).persistForDevice(context, gbDevice, emotionsSamples); } catch (Exception e) { - LOG.error("Cannot save skin emotions samples, continue"); + LOG.error("Cannot save emotions samples, continue"); + } + } else if (dictClass == HuaweiDictTypes.SLEEP_APNEA_CLASS) { + List sleepApneaSamples = new ArrayList<>(); + for (HuaweiP2PDataDictionarySyncService.DictData dt : dictData) { + long timestamp = dt.getStartTimestamp(); + long lastTime = Math.max(dt.getEndTimestamp(), dt.getModifyTimestamp()); + Integer level = null; + for (HuaweiP2PDataDictionarySyncService.DictData.DictDataValue val : dt.getData()) { + if (val.getTag() == 10) { + if (val.getDataType() == HuaweiDictTypes.SLEEP_APNEA_LEVEL_VALUE) { + int value = (int) HuaweiUtil.convBytes2Double(val.getValue()); + if (value == 1 || value == 2 || value == 3 || value == 4) { + level = value; + } else { + LOG.info("sleep apnea invalid value: {}", value); + } + } else { + LOG.info("sleep apnea unknown data type: {}", val.getDataType()); + } + } else { + LOG.info("sleep apnea unsupported tag: {}", val.getTag()); + } + } + if(level != null) { + LOG.info("APNEA timestamp: {} lastTime: {} level: {}", new Date(timestamp), lastTime, level); + HuaweiSleepApneaSample sample = new HuaweiSleepApneaSample(); + sample.setTimestamp(timestamp); + sample.setLastTimestamp(lastTime); + sample.setLevel(level); + sleepApneaSamples.add(sample); + } + } + try (DBHandler db = GBApplication.acquireDB()) { + final DaoSession session = db.getDaoSession(); + new HuaweiSleepApneaSampleProvider(gbDevice, session).persistForDevice(context, gbDevice, sleepApneaSamples); + } catch (Exception e) { + LOG.error("Cannot save sleep apnea samples, continue"); } } } @@ -3391,13 +3454,10 @@ public class HuaweiSupportProvider { public void onTestNewFunction() { - AsyncTask.execute(new Runnable() { - @Override - public void run() { - long startTime = System.nanoTime(); - boolean ret = HuaweiCompatTemperatureSampleProvider.migrateOldData(); - LOG.info("Migrating: {} Took : {} ms", ret ? "OK" : "Error", ((System.nanoTime() - startTime) / 1000000.0)); - } + AsyncTask.execute(() -> { + long startTime = System.nanoTime(); + boolean ret = HuaweiCompatTemperatureSampleProvider.migrateOldData(); + LOG.info("Migrating: {} Took : {} ms", ret ? "OK" : "Error", ((System.nanoTime() - startTime) / 1000000.0)); }); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/p2p/HuaweiP2PDataDictionarySyncService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/p2p/HuaweiP2PDataDictionarySyncService.java index 99782ff2cb..c42df12c46 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/p2p/HuaweiP2PDataDictionarySyncService.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/p2p/HuaweiP2PDataDictionarySyncService.java @@ -91,6 +91,8 @@ public class HuaweiP2PDataDictionarySyncService extends HuaweiBaseP2PService { result.add(cl); } else if(cl == HuaweiDictTypes.EMOTION_CLASS && coordinator.supportsEmotion()) { result.add(cl); + } else if(cl == HuaweiDictTypes.SLEEP_APNEA_CLASS && coordinator.supportsSleepApnea()) { + result.add(cl); } } return result;