From 28e673415fb3978ce67ef648847d6850ed7bebec Mon Sep 17 00:00:00 2001 From: ahormann Date: Sun, 15 Oct 2023 13:37:41 +0000 Subject: [PATCH] Support for Femometer Vinca 2 and HealthThermometerProfile (#3369) Co-authored-by: ahormann Co-committed-by: ahormann --- .../gadgetbridge/daogen/GBDaoGenerator.java | 18 +- .../DeviceSettingsPreferenceConst.java | 1 + .../DeviceSpecificSettingsFragment.java | 1 + .../devices/AbstractDeviceCoordinator.java | 6 + .../devices/DeviceCoordinator.java | 6 + .../FemometerVinca2DeviceCoordinator.java | 112 +++++++ .../FemometerVinca2SampleProvider.java | 56 ++++ .../entities/AbstractTemperatureSample.java | 37 +++ .../gadgetbridge/model/DeviceType.java | 3 + .../gadgetbridge/model/TemperatureSample.java | 29 ++ .../service/AbstractDeviceSupport.java | 4 +- .../service/btle/actions/NotifyAction.java | 6 +- .../service/btle/profiles/ValueDecoder.java | 4 + .../HealthThermometerProfile.java | 141 ++++++++ .../healthThermometer/TemperatureInfo.java | 85 +++++ .../FemometerVinca2DeviceSupport.java | 300 ++++++++++++++++++ .../res/drawable/ic_device_thermometer.xml | 6 + .../ic_device_thermometer_disabled.xml | 6 + app/src/main/res/values/arrays.xml | 12 + app/src/main/res/values/strings.xml | 6 + .../main/res/xml/devicesettings_femometer.xml | 11 + 21 files changed, 845 insertions(+), 5 deletions(-) create mode 100755 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/femometer/FemometerVinca2DeviceCoordinator.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/femometer/FemometerVinca2SampleProvider.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/entities/AbstractTemperatureSample.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/TemperatureSample.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/healthThermometer/HealthThermometerProfile.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/healthThermometer/TemperatureInfo.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/femometer/FemometerVinca2DeviceSupport.java create mode 100644 app/src/main/res/drawable/ic_device_thermometer.xml create mode 100644 app/src/main/res/drawable/ic_device_thermometer_disabled.xml create mode 100644 app/src/main/res/xml/devicesettings_femometer.xml diff --git a/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java b/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java index 8cabb6c52..2a2acbc52 100644 --- a/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java +++ b/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java @@ -38,12 +38,14 @@ public class GBDaoGenerator { private static final String SAMPLE_STEPS = "steps"; private static final String SAMPLE_RAW_KIND = "rawKind"; private static final String SAMPLE_HEART_RATE = "heartRate"; + private static final String SAMPLE_TEMPERATURE = "temperature"; + private static final String SAMPLE_TEMPERATURE_TYPE = "temperatureType"; private static final String TIMESTAMP_FROM = "timestampFrom"; private static final String TIMESTAMP_TO = "timestampTo"; public static void main(String[] args) throws Exception { - final Schema schema = new Schema(59, MAIN_PACKAGE + ".entities"); + final Schema schema = new Schema(60, MAIN_PACKAGE + ".entities"); Entity userAttributes = addUserAttributes(schema); Entity user = addUserInfo(schema, userAttributes); @@ -101,6 +103,7 @@ public class GBDaoGenerator { addWena3HeartRateSample(schema, user, device); addWena3Vo2Sample(schema, user, device); addWena3StressSample(schema, user, device); + addFemometerVinca2TemperatureSample(schema, user, device); addCalendarSyncState(schema, device); addAlarms(schema, user, device); @@ -938,4 +941,17 @@ public class GBDaoGenerator { perAppSetting.addStringProperty("vibrationRepetition"); return perAppSetting; } + + private static void addTemperatureProperties(Entity activitySample) { + activitySample.addFloatProperty(SAMPLE_TEMPERATURE).notNull().codeBeforeGetter(OVERRIDE); + activitySample.addIntProperty(SAMPLE_TEMPERATURE_TYPE).notNull().codeBeforeGetter(OVERRIDE); + } + + private static Entity addFemometerVinca2TemperatureSample(Schema schema, Entity user, Entity device) { + Entity sample = addEntity(schema, "FemometerVinca2TemperatureSample"); + addCommonTimeSampleProperties("AbstractTemperatureSample", sample, user, device); + addTemperatureProperties(sample); + return sample; + } + } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSettingsPreferenceConst.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSettingsPreferenceConst.java index dc2e01a1b..7c6bcd6bf 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSettingsPreferenceConst.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSettingsPreferenceConst.java @@ -362,4 +362,5 @@ public class DeviceSettingsPreferenceConst { public static final String PREF_VOICE_SERVICE_LANGUAGE = "voice_service_language"; public static final String PREF_TEMPERATURE_SCALE_CF = "temperature_scale_cf"; + public static final String PREF_FEMOMETER_MEASUREMENT_MODE = "femometer_measurement_mode"; } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSpecificSettingsFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSpecificSettingsFragment.java index 1a43252f6..696d43989 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSpecificSettingsFragment.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSpecificSettingsFragment.java @@ -506,6 +506,7 @@ public class DeviceSpecificSettingsFragment extends AbstractPreferenceFragment i addPreferenceHandlerFor(PREF_SONY_SPEAK_TO_CHAT_FOCUS_ON_VOICE); addPreferenceHandlerFor(PREF_SONY_SPEAK_TO_CHAT_TIMEOUT); addPreferenceHandlerFor(PREF_SONY_CONNECT_TWO_DEVICES); + addPreferenceHandlerFor(PREF_FEMOMETER_MEASUREMENT_MODE); addPreferenceHandlerFor(PREF_QC35_NOISE_CANCELLING_LEVEL); addPreferenceHandlerFor(PREF_USER_FITNESS_GOAL); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractDeviceCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractDeviceCoordinator.java index d99749459..4624e0420 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractDeviceCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractDeviceCoordinator.java @@ -71,6 +71,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.PaiSample; import nodomain.freeyourgadget.gadgetbridge.model.SleepRespiratoryRateSample; import nodomain.freeyourgadget.gadgetbridge.model.Spo2Sample; import nodomain.freeyourgadget.gadgetbridge.model.StressSample; +import nodomain.freeyourgadget.gadgetbridge.model.TemperatureSample; import nodomain.freeyourgadget.gadgetbridge.service.ServiceDeviceSupport; import nodomain.freeyourgadget.gadgetbridge.util.Prefs; @@ -190,6 +191,11 @@ public abstract class AbstractDeviceCoordinator implements DeviceCoordinator { return null; } + @Override + public TimeSampleProvider getTemperatureSampleProvider(GBDevice device, DaoSession session) { + return null; + } + @Override public TimeSampleProvider getSpo2SampleProvider(GBDevice device, DaoSession session) { return null; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceCoordinator.java index e6d310550..5f2267a37 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceCoordinator.java @@ -51,6 +51,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.PaiSample; import nodomain.freeyourgadget.gadgetbridge.model.SleepRespiratoryRateSample; 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.ServiceDeviceSupport; @@ -235,6 +236,11 @@ public interface DeviceCoordinator { */ TimeSampleProvider getStressSampleProvider(GBDevice device, DaoSession session); + /** + * Returns the sample provider for temperature data, for the device being supported. + */ + TimeSampleProvider getTemperatureSampleProvider(GBDevice device, DaoSession session); + /** * Returns the sample provider for SpO2 data, for the device being supported. */ diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/femometer/FemometerVinca2DeviceCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/femometer/FemometerVinca2DeviceCoordinator.java new file mode 100755 index 000000000..7b1f19ea5 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/femometer/FemometerVinca2DeviceCoordinator.java @@ -0,0 +1,112 @@ +/* Copyright (C) 2023 Alicia Hormann + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ +package nodomain.freeyourgadget.gadgetbridge.devices.femometer; + +import android.app.Activity; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.regex.Pattern; + +import de.greenrobot.dao.query.QueryBuilder; +import nodomain.freeyourgadget.gadgetbridge.GBException; +import nodomain.freeyourgadget.gadgetbridge.R; +import nodomain.freeyourgadget.gadgetbridge.devices.AbstractDeviceCoordinator; +import nodomain.freeyourgadget.gadgetbridge.devices.TimeSampleProvider; +import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; +import nodomain.freeyourgadget.gadgetbridge.entities.Device; +import nodomain.freeyourgadget.gadgetbridge.entities.FemometerVinca2TemperatureSample; +import nodomain.freeyourgadget.gadgetbridge.entities.FemometerVinca2TemperatureSampleDao; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.service.DeviceSupport; +import nodomain.freeyourgadget.gadgetbridge.service.devices.femometer.FemometerVinca2DeviceSupport; + +public class FemometerVinca2DeviceCoordinator extends AbstractDeviceCoordinator { + @Override + public String getManufacturer() { + return "Joytech Healthcare"; + } + + @NonNull + @Override + public Class getDeviceSupportClass() { + return FemometerVinca2DeviceSupport.class; + } + + @Override + public TimeSampleProvider getTemperatureSampleProvider(GBDevice device, DaoSession session) { + return new FemometerVinca2SampleProvider(device, session); + } + + @Override + protected Pattern getSupportedDeviceName() { + return Pattern.compile("BM-Vinca2"); + } + + + @Override + public int getDeviceNameResource() { + return R.string.devicetype_femometer_vinca2; + } + + @Override + public int getDefaultIconResource() { + return R.drawable.ic_device_thermometer; + } + + @Override + public int getDisabledIconResource() { + return R.drawable.ic_device_thermometer_disabled; + } + + + @Override + protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) throws GBException { + Long deviceId = device.getId(); + QueryBuilder qb = session.getFemometerVinca2TemperatureSampleDao().queryBuilder(); + qb.where(FemometerVinca2TemperatureSampleDao.Properties.DeviceId.eq(deviceId)).buildDelete().executeDeleteWithoutDetachingEntities(); + } + + + @Override + public int getBondingStyle(){ + return BONDING_STYLE_NONE; + } + + @Nullable + @Override + public Class getPairingActivity() { + return null; + } + + @Override + public int getAlarmSlotCount(final GBDevice device) { + return 1; + } + + @Override + public int[] getSupportedDeviceSpecificSettings(GBDevice device) { + return new int[]{ + R.xml.devicesettings_volume, + R.xml.devicesettings_femometer, + R.xml.devicesettings_temperature_scale_cf, + }; + } + +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/femometer/FemometerVinca2SampleProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/femometer/FemometerVinca2SampleProvider.java new file mode 100644 index 000000000..1f1f278f3 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/femometer/FemometerVinca2SampleProvider.java @@ -0,0 +1,56 @@ +/* Copyright (C) 2023 Alicia Hormann + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ +package nodomain.freeyourgadget.gadgetbridge.devices.femometer; + +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.FemometerVinca2TemperatureSample; +import nodomain.freeyourgadget.gadgetbridge.entities.FemometerVinca2TemperatureSampleDao; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; + +public class FemometerVinca2SampleProvider extends AbstractTimeSampleProvider { + + public FemometerVinca2SampleProvider(GBDevice device, DaoSession session) { + super(device, session); + } + + @Override + @NonNull + public AbstractDao getSampleDao() { + return getSession().getFemometerVinca2TemperatureSampleDao(); + } + + @NonNull + protected Property getTimestampSampleProperty() { + return FemometerVinca2TemperatureSampleDao.Properties.Timestamp; + } + + @NonNull + protected Property getDeviceIdentifierSampleProperty() { + return FemometerVinca2TemperatureSampleDao.Properties.DeviceId; + } + + @Override + public FemometerVinca2TemperatureSample createSample() { + return new FemometerVinca2TemperatureSample(); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/entities/AbstractTemperatureSample.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/entities/AbstractTemperatureSample.java new file mode 100644 index 000000000..40b75c3bc --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/entities/AbstractTemperatureSample.java @@ -0,0 +1,37 @@ +/* Copyright (C) 2023 Alicia Hormann + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ +package nodomain.freeyourgadget.gadgetbridge.entities; + +import androidx.annotation.NonNull; + +import nodomain.freeyourgadget.gadgetbridge.model.TemperatureSample; +import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils; + +public abstract class AbstractTemperatureSample extends AbstractTimeSample implements TemperatureSample { + @NonNull + @Override + public String toString() { + return getClass().getSimpleName() + "{" + + "timestamp=" + DateTimeUtils.formatDateTime(DateTimeUtils.parseTimestampMillis(getTimestamp())) + + ", temperature=" + getTemperature() + + ", temperatureType=" + getTemperatureType() + + ", userId=" + getUserId() + + ", deviceId=" + getDeviceId() + + "}"; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java index 5250555c6..8156056d1 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java @@ -35,6 +35,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.casio.gbx100.CasioGBX100Devi import nodomain.freeyourgadget.gadgetbridge.devices.casio.gwb5600.CasioGMWB5000DeviceCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.casio.gwb5600.CasioGWB5600DeviceCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.domyos.DomyosT540Coordinator; +import nodomain.freeyourgadget.gadgetbridge.devices.femometer.FemometerVinca2DeviceCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProDeviceCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.flipper.zero.FlipperZeroCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.galaxy_buds.GalaxyBuds2DeviceCoordinator; @@ -265,6 +266,8 @@ public enum DeviceType { SOFLOW_SO6(550, SoFlowCoordinator.class), WITHINGS_STEEL_HR(560, WithingsSteelHRDeviceCoordinator.class), SONY_WENA_3(570, SonyWena3Coordinator.class), + + FEMOMETER_VINCA2(580, FemometerVinca2DeviceCoordinator.class), TEST(1000, TestDeviceCoordinator.class); private final int key; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/TemperatureSample.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/TemperatureSample.java new file mode 100644 index 000000000..2eac5acbe --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/TemperatureSample.java @@ -0,0 +1,29 @@ +/* Copyright (C) 2023 Alicia Hormann + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ +package nodomain.freeyourgadget.gadgetbridge.model; + +public interface TemperatureSample extends TimeSample { + /** + * Returns the temperature value. + */ + float getTemperature(); + /** + * Returns the temperature type (the position on the body where the measurement was taken). + */ + int getTemperatureType(); +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/AbstractDeviceSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/AbstractDeviceSupport.java index 85ec34388..e966254ed 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/AbstractDeviceSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/AbstractDeviceSupport.java @@ -310,7 +310,9 @@ public abstract class AbstractDeviceSupport implements DeviceSupport { if (gbDevice == null) { return; } - gbDevice.setFirmwareVersion(infoEvent.fwVersion); + if (infoEvent.fwVersion != null) { + gbDevice.setFirmwareVersion(infoEvent.fwVersion); + } if (infoEvent.fwVersion2 != null) { gbDevice.setFirmwareVersion2(infoEvent.fwVersion2); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/actions/NotifyAction.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/actions/NotifyAction.java index 791f8ca2a..0e923fb52 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/actions/NotifyAction.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/actions/NotifyAction.java @@ -28,7 +28,6 @@ import org.slf4j.LoggerFactory; import nodomain.freeyourgadget.gadgetbridge.service.btle.BtLEAction; import static nodomain.freeyourgadget.gadgetbridge.service.btle.GattDescriptor.UUID_DESCRIPTOR_GATT_CLIENT_CHARACTERISTIC_CONFIGURATION; - /** * Enables or disables notifications for a given GATT characteristic. * The result will be made available asynchronously through the @@ -53,15 +52,16 @@ public class NotifyAction extends BtLEAction { if (notifyDescriptor != null) { int properties = getCharacteristic().getProperties(); if ((properties & BluetoothGattCharacteristic.PROPERTY_NOTIFY) > 0) { - LOG.debug("use NOTIFICATION"); + LOG.debug("use NOTIFICATION for Characteristic " + getCharacteristic().getUuid()); notifyDescriptor.setValue(enableFlag ? BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE : BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE); result = gatt.writeDescriptor(notifyDescriptor); } else if ((properties & BluetoothGattCharacteristic.PROPERTY_INDICATE) > 0) { - LOG.debug("use INDICATION"); + LOG.debug("use INDICATION for Characteristic " + getCharacteristic().getUuid()); notifyDescriptor.setValue(enableFlag ? BluetoothGattDescriptor.ENABLE_INDICATION_VALUE : BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE); result = gatt.writeDescriptor(notifyDescriptor); hasWrittenDescriptor = true; } else { + LOG.debug("use neither NOTIFICATION nor INDICATION for Characteristic " + getCharacteristic().getUuid()); hasWrittenDescriptor = false; } } else { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/ValueDecoder.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/ValueDecoder.java index 161b54ed1..70689dbad 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/ValueDecoder.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/ValueDecoder.java @@ -26,6 +26,10 @@ import nodomain.freeyourgadget.gadgetbridge.service.btle.GattCharacteristic; public class ValueDecoder { private static final Logger LOG = LoggerFactory.getLogger(ValueDecoder.class); + public static int decodeInt(BluetoothGattCharacteristic characteristic) { + return characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, 0); + } + public static int decodePercent(BluetoothGattCharacteristic characteristic) { int percent = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, 0); if (percent > 100 || percent < 0) { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/healthThermometer/HealthThermometerProfile.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/healthThermometer/HealthThermometerProfile.java new file mode 100644 index 000000000..e749f6d84 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/healthThermometer/HealthThermometerProfile.java @@ -0,0 +1,141 @@ +/* Copyright (C) 2023 Alicia Hormann + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ +package nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.healthThermometer; + +import android.bluetooth.BluetoothGatt; +import android.bluetooth.BluetoothGattCharacteristic; +import android.content.Intent; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.UUID; + +import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport; +import nodomain.freeyourgadget.gadgetbridge.service.btle.GattCharacteristic; +import nodomain.freeyourgadget.gadgetbridge.service.btle.GattService; +import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; +import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.AbstractBleProfile; +import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.ValueDecoder; + +/*** + * This class handles the HealthThermometer as implemented on the Femometer Vinca II. + * This might or might not be up to GATT standard. + * @param + */ +public class HealthThermometerProfile extends AbstractBleProfile { + private static final Logger LOG = LoggerFactory.getLogger(HealthThermometerProfile.class); + + private static final String ACTION_PREFIX = HealthThermometerProfile.class.getName() + "_"; + + public static final String ACTION_TEMPERATURE_INFO = ACTION_PREFIX + "TEMPERATURE_INFO"; + public static final String EXTRA_TEMPERATURE_INFO = "TEMPERATURE_INFO"; + + public static final UUID SERVICE_UUID = GattService.UUID_SERVICE_HEALTH_THERMOMETER; + public static final UUID UUID_CHARACTERISTIC_TEMPERATURE_MEASUREMENT = GattCharacteristic.UUID_CHARACTERISTIC_TEMPERATURE_MEASUREMENT; + public static final UUID UUID_CHARACTERISTIC_MEASUREMENT_INTERVAL = GattCharacteristic.UUID_CHARACTERISTIC_MEASUREMENT_INTERVAL; + private final TemperatureInfo temperatureInfo = new TemperatureInfo(); + + public HealthThermometerProfile(T support) { + super(support); + } + + public void requestMeasurementInterval(TransactionBuilder builder) { + builder.read(getCharacteristic(UUID_CHARACTERISTIC_MEASUREMENT_INTERVAL)); + } + + public void setMeasurementInterval(TransactionBuilder builder, byte[] value) { + builder.write(getCharacteristic(GattCharacteristic.UUID_CHARACTERISTIC_MEASUREMENT_INTERVAL), value); + } + + @Override + public void enableNotify(TransactionBuilder builder, boolean enable) { + builder.notify(getCharacteristic(UUID_CHARACTERISTIC_TEMPERATURE_MEASUREMENT), enable); + } + + @Override + public boolean onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { + if (status == BluetoothGatt.GATT_SUCCESS) { + UUID charUuid = characteristic.getUuid(); + if (charUuid.equals(UUID_CHARACTERISTIC_MEASUREMENT_INTERVAL)) { + handleMeasurementInterval(gatt, characteristic); + return true; + } else if (charUuid.equals(UUID_CHARACTERISTIC_TEMPERATURE_MEASUREMENT)) { + handleTemperatureMeasurement(gatt, characteristic); + return true; + } else { + LOG.info("Unexpected onCharacteristicRead: " + GattCharacteristic.toString(characteristic)); + } + } else { + LOG.warn("error reading from characteristic:" + GattCharacteristic.toString(characteristic)); + } + return false; + } + + @Override + public boolean onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { + return onCharacteristicRead(gatt, characteristic, BluetoothGatt.GATT_SUCCESS); + } + + + private void handleMeasurementInterval(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { + // todo: not implemented + LOG.debug("Health thermometer received Measurement Interval: " + ValueDecoder.decodeInt(characteristic)); + } + + private void handleTemperatureMeasurement(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { + /* + * This metadata contains as bits: + * the unit (celsius (0) or fahrenheit (1)) (bit 7 (last bit)) + * if a timestamp is present (1) or not present (0) (bit 6) + * if a temperature type is present (1) or not present (0) (bit 5) + */ + byte metadata = characteristic.getValue()[0]; + // todo: evaluate this byte to enable support for devices without timestamp or temperature-type + + int year = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, 5); + int month = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, 7); + int day = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, 8); + int hour = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, 9); + int minute = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, 10); + int second = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, 11); + + Calendar c = GregorianCalendar.getInstance(); + c.set(year, month - 1, day, hour, minute, second); + Date date = c.getTime(); + + float temperature = characteristic.getFloatValue(BluetoothGattCharacteristic.FORMAT_FLOAT, 1); // bytes 1 - 4 + int temperature_type = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, 12); // encodes where the measurement was taken + + LOG.debug("Received measurement of " + temperature + "° with Timestamp " + date + ", metadata is " + Integer.toBinaryString((metadata & 0xFF) + 0x100).substring(1)); + + temperatureInfo.setTemperature(temperature); + temperatureInfo.setTemperatureType(temperature_type); + temperatureInfo.setTimestamp(date); + notify(createIntent(temperatureInfo)); + } + + private Intent createIntent(TemperatureInfo temperatureInfo) { + Intent intent = new Intent(ACTION_TEMPERATURE_INFO); + intent.putExtra(EXTRA_TEMPERATURE_INFO, temperatureInfo); + return intent; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/healthThermometer/TemperatureInfo.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/healthThermometer/TemperatureInfo.java new file mode 100644 index 000000000..4d5781b2e --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/healthThermometer/TemperatureInfo.java @@ -0,0 +1,85 @@ +/* Copyright (C) 2023 Alicia Hormann + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ +package nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.healthThermometer; + +import android.os.Parcel; +import android.os.Parcelable; + +import java.util.Date; + +public class TemperatureInfo implements Parcelable{ + + private float temperature; + private int temperatureType; + private Date timestamp; + + public TemperatureInfo() { + } + + protected TemperatureInfo(Parcel in) { + timestamp = new Date(in.readLong()); + temperature = in.readFloat(); + temperatureType = in.readInt(); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeLong(timestamp.getTime()); + dest.writeFloat(temperature); + dest.writeInt(temperatureType); + } + + @Override + public int describeContents() { + return 0; + } + + public static final Creator CREATOR = new Creator() { + @Override + public TemperatureInfo createFromParcel(Parcel in) { + return new TemperatureInfo(in); + } + + @Override + public TemperatureInfo[] newArray(int size) { + return new TemperatureInfo[size]; + } + }; + + public float getTemperature() { + return temperature; + } + public Date getTimestamp() { + return timestamp; + } + public void setTimestamp(Date date) { + timestamp = date; + } + + public void setTemperature(float temperature) { + this.temperature = temperature; + } + + public int getTemperatureType() { + return temperatureType; + } + + public void setTemperatureType(int temperatureType) { + this.temperatureType = temperatureType; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/femometer/FemometerVinca2DeviceSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/femometer/FemometerVinca2DeviceSupport.java new file mode 100644 index 000000000..3315baa89 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/femometer/FemometerVinca2DeviceSupport.java @@ -0,0 +1,300 @@ +/* Copyright (C) 2023 Alicia Hormann + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ +package nodomain.freeyourgadget.gadgetbridge.service.devices.femometer; + +import android.bluetooth.BluetoothGattCharacteristic; +import android.content.SharedPreferences; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.UUID; + +import nodomain.freeyourgadget.gadgetbridge.GBApplication; +import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst; +import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; +import nodomain.freeyourgadget.gadgetbridge.database.DBHelper; +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo; +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventVersionInfo; +import nodomain.freeyourgadget.gadgetbridge.devices.femometer.FemometerVinca2SampleProvider; +import nodomain.freeyourgadget.gadgetbridge.entities.FemometerVinca2TemperatureSample; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.model.Alarm; +import nodomain.freeyourgadget.gadgetbridge.model.BatteryState; +import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport; +import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions; +import nodomain.freeyourgadget.gadgetbridge.service.btle.GattCharacteristic; +import nodomain.freeyourgadget.gadgetbridge.service.btle.GattService; +import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; +import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction; +import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.IntentListener; +import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.battery.BatteryInfo; +import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.battery.BatteryInfoProfile; +import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.deviceinfo.DeviceInfo; +import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.deviceinfo.DeviceInfoProfile; +import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.healthThermometer.HealthThermometerProfile; +import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.healthThermometer.TemperatureInfo; + +public class FemometerVinca2DeviceSupport extends AbstractBTLEDeviceSupport { + + private final DeviceInfoProfile deviceInfoProfile; + private final BatteryInfoProfile batteryInfoProfile; + private final HealthThermometerProfile healthThermometerProfile; + private static final Logger LOG = LoggerFactory.getLogger(FemometerVinca2DeviceSupport.class); + + public static final UUID UNKNOWN_SERVICE_UUID = UUID.fromString((String.format(AbstractBTLEDeviceSupport.BASE_UUID, "fef5"))); + // Characteristic 8082caa8-41a6-4021-91c6-56f9b954cc34 READ WRITE + // Characteristic 9d84b9a3-000c-49d8-9183-855b673fda31 READ WRITE + // Characteristic 457871e8-d516-4ca1-9116-57d0b17b9cb2 READ WRITE NO RESPONSE WRITE + // Characteristic 5f78df94-798c-46f5-990a-b3eb6a065c88 READ NOTIFY + + public static final UUID CONFIGURATION_SERVICE_UUID = UUID.fromString("0f0e0d0c-0b0a-0908-0706-050403020100"); + public static final UUID CONFIGURATION_SERVICE_ALARM_CHARACTERISTIC = UUID.fromString("1f1e1d1c-1b1a-1918-1716-151413121110"); // READ WRITE + public static final UUID CONFIGURATION_SERVICE_SETTING_CHARACTERISTIC = UUID.fromString("2f2e2d2c-2b2a-2928-2726-252423222120"); // WRITE + public static final UUID CONFIGURATION_SERVICE_INDICATION_CHARACTERISTIC = UUID.fromString("3f3e3d3c-3b3a-3938-3736-353433323130"); // INDICATE + + public FemometerVinca2DeviceSupport() { + super(LOG); + + /// Initialize Services + addSupportedService(GattService.UUID_SERVICE_GENERIC_ACCESS); + addSupportedService(GattService.UUID_SERVICE_GENERIC_ATTRIBUTE); + addSupportedService(GattService.UUID_SERVICE_BATTERY_SERVICE); + addSupportedService(GattService.UUID_SERVICE_DEVICE_INFORMATION); + addSupportedService(GattService.UUID_SERVICE_HEALTH_THERMOMETER); + addSupportedService(GattService.UUID_SERVICE_CURRENT_TIME); + addSupportedService(GattService.UUID_SERVICE_REFERENCE_TIME_UPDATE); + addSupportedService(UNKNOWN_SERVICE_UUID); + addSupportedService(CONFIGURATION_SERVICE_UUID); + + /// Device Info + IntentListener deviceInfoListener = intent -> { + String action = intent.getAction(); + if (DeviceInfoProfile.ACTION_DEVICE_INFO.equals(action)) { + DeviceInfo info = intent.getParcelableExtra(DeviceInfoProfile.EXTRA_DEVICE_INFO); + if (info == null) return; + GBDeviceEventVersionInfo versionCmd = new GBDeviceEventVersionInfo(); + versionCmd.hwVersion = info.getHardwareRevision(); + versionCmd.fwVersion = info.getSoftwareRevision(); // firmwareRevision always reported as null + handleGBDeviceEvent(versionCmd); + } + }; + + deviceInfoProfile = new DeviceInfoProfile<>(this); + deviceInfoProfile.addListener(deviceInfoListener); + addSupportedProfile(deviceInfoProfile); + + /// Battery + IntentListener batteryListener = intent -> { + BatteryInfo info = intent.getParcelableExtra(BatteryInfoProfile.EXTRA_BATTERY_INFO); + if (info == null) return; + GBDeviceEventBatteryInfo batteryEvent = new GBDeviceEventBatteryInfo(); + batteryEvent.state = BatteryState.BATTERY_NORMAL; + batteryEvent.level = info.getPercentCharged(); + evaluateGBDeviceEvent(batteryEvent); + handleGBDeviceEvent(batteryEvent); + }; + batteryInfoProfile = new BatteryInfoProfile<>(this); + batteryInfoProfile.addListener(batteryListener); + addSupportedProfile(batteryInfoProfile); + + + /// Temperature + IntentListener temperatureListener = intent -> { + TemperatureInfo info = intent.getParcelableExtra(HealthThermometerProfile.EXTRA_TEMPERATURE_INFO); + if (info == null) return; + handleMeasurement(info); + }; + healthThermometerProfile = new HealthThermometerProfile<>(this); + healthThermometerProfile.addListener(temperatureListener); + addSupportedProfile(healthThermometerProfile); + } + + @Override + public boolean useAutoConnect() { + return false; + } + + /** + * @param data An int smaller equal 255 (0xff) + */ + private byte[] byteArray(int data) { + return new byte[]{(byte) data}; + } + + @Override + protected TransactionBuilder initializeDevice(TransactionBuilder builder) { + builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZING, getContext())); + + // Init Battery + batteryInfoProfile.requestBatteryInfo(builder); + batteryInfoProfile.enableNotify(builder, true); + + // Init Device Info + getDevice().setFirmwareVersion("N/A"); + getDevice().setFirmwareVersion2("N/A"); + deviceInfoProfile.requestDeviceInfo(builder); + + // Mystery stuff that happens in original app, not sure if its required + BluetoothGattCharacteristic c2 = getCharacteristic(CONFIGURATION_SERVICE_SETTING_CHARACTERISTIC); + builder.write(c2, byteArray(0x21)); + builder.write(c2, byteArray(0x02)); + builder.write(c2, byteArray(0x03)); + builder.write(c2, byteArray(0x05)); + + // Sync Time + setCurrentTime(builder); + + // Init Thermometer + builder.notify(getCharacteristic(CONFIGURATION_SERVICE_INDICATION_CHARACTERISTIC), true); + healthThermometerProfile.enableNotify(builder, true); + healthThermometerProfile.setMeasurementInterval(builder, new byte[]{(byte) 0x01, (byte) 0x00}); + + // mark the device as initialized + builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZED, getContext())); + return builder; + } + + @Override + public void onSetTime() { + TransactionBuilder builder = new TransactionBuilder("set time"); + setCurrentTime(builder); + builder.queue(getQueue()); + } + + private void setCurrentTime(TransactionBuilder builder) { + // Same Code as in PineTime (without the local time) + GregorianCalendar now = BLETypeConversions.createCalendar(); + byte[] bytesCurrentTime = BLETypeConversions.calendarToCurrentTime(now, 0); + builder.write(getCharacteristic(GattCharacteristic.UUID_CHARACTERISTIC_CURRENT_TIME), bytesCurrentTime); + } + + @Override + public void onSetAlarms(ArrayList alarms) { + try { + TransactionBuilder builder = performInitialized("applyThermometerSetting"); + + Alarm alarm = alarms.get(0); + byte[] alarm_bytes = new byte[] { + (byte) (alarm.getEnabled()? 0x01 : 0x00), // first byte 01/00: turn alarm on/off + (byte) alarm.getHour(), // second byte: hour + (byte) alarm.getMinute() // third byte: minute + }; + + builder.write(getCharacteristic(CONFIGURATION_SERVICE_ALARM_CHARACTERISTIC), alarm_bytes); + builder.write(getCharacteristic(CONFIGURATION_SERVICE_SETTING_CHARACTERISTIC), byteArray(0x01)); + // read-request on char1 results in given alarm + builder.queue(getQueue()); + } catch (IOException e) { + LOG.warn(" Unable to apply setting ", e); + } + } + + @Override + public void onSendConfiguration(String config) { + TransactionBuilder builder; + SharedPreferences sharedPreferences = GBApplication.getDeviceSpecificSharedPrefs(this.getDevice().getAddress()); + LOG.info(" onSendConfiguration: " + config); + try { + builder = performInitialized("sendConfig: " + config); + switch (config) { + case DeviceSettingsPreferenceConst.PREF_FEMOMETER_MEASUREMENT_MODE: + setMeasurementMode(sharedPreferences); + break; + case DeviceSettingsPreferenceConst.PREF_VOLUME: + setVolume(sharedPreferences); + break; + case DeviceSettingsPreferenceConst.PREF_TEMPERATURE_SCALE_CF: + String scale = sharedPreferences.getString(DeviceSettingsPreferenceConst.PREF_TEMPERATURE_SCALE_CF, "c"); + int value = "c".equals(scale) ? 0x0a : 0x0b; + applySetting(byteArray(value), null); + } + builder.queue(getQueue()); + } catch (IOException e) { + e.printStackTrace(); + } + } + + /** Set Measurement Mode + * modes (0- quick, 1- normal, 2- long) + */ + private void setMeasurementMode(SharedPreferences sharedPreferences) { + String measurementMode = sharedPreferences.getString(DeviceSettingsPreferenceConst.PREF_FEMOMETER_MEASUREMENT_MODE, "normal"); + byte[] confirmation = byteArray(0x1e); + switch (measurementMode) { + case "quick": + applySetting(byteArray(0x1a), confirmation); + break; + case "normal": + applySetting(byteArray(0x1c), confirmation); + break; + case "precise": + applySetting(byteArray(0x1d), confirmation); + break; + } + } + + /** Set Volume + * volumes 0-30 (0-10: quiet, 11-20: normal, 21-30: loud) + */ + private void setVolume(SharedPreferences sharedPreferences) { + int volume = sharedPreferences.getInt(DeviceSettingsPreferenceConst.PREF_VOLUME, 50); + byte[] confirmation = byteArray(0xfd); + if (volume < 11) { + applySetting(byteArray(0x09), confirmation); + } else if (volume < 21) { + applySetting(byteArray(0x14), confirmation); + } else { + applySetting(byteArray(0x16), confirmation); + } + } + + private void applySetting(byte[] value, byte[] confirmation) { + try { + TransactionBuilder builder = performInitialized("applyThermometerSetting"); + builder.write(getCharacteristic(CONFIGURATION_SERVICE_SETTING_CHARACTERISTIC), value); + if (confirmation != null) { + builder.write(getCharacteristic(CONFIGURATION_SERVICE_SETTING_CHARACTERISTIC), confirmation); + } + builder.queue(getQueue()); + } catch (IOException e) { + LOG.warn(" Unable to apply setting ", e); + } + } + + private void handleMeasurement(TemperatureInfo info) { + Date timestamp = info.getTimestamp(); + float temperature = info.getTemperature(); + int temperatureType = info.getTemperatureType(); + try (DBHandler db = GBApplication.acquireDB()) { + Long userId = DBHelper.getUser(db.getDaoSession()).getId(); + Long deviceId = DBHelper.getDevice(getDevice(), db.getDaoSession()).getId(); + long time = timestamp.getTime(); + + FemometerVinca2SampleProvider sampleProvider = new FemometerVinca2SampleProvider(getDevice(), db.getDaoSession()); + FemometerVinca2TemperatureSample temperatureSample = new FemometerVinca2TemperatureSample(time, deviceId, userId, temperature, temperatureType); + sampleProvider.addSample(temperatureSample); + } catch (Exception e) { + LOG.error("Error acquiring database", e); + } + } +} diff --git a/app/src/main/res/drawable/ic_device_thermometer.xml b/app/src/main/res/drawable/ic_device_thermometer.xml new file mode 100644 index 000000000..7baeec1af --- /dev/null +++ b/app/src/main/res/drawable/ic_device_thermometer.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_device_thermometer_disabled.xml b/app/src/main/res/drawable/ic_device_thermometer_disabled.xml new file mode 100644 index 000000000..b97f8789e --- /dev/null +++ b/app/src/main/res/drawable/ic_device_thermometer_disabled.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index 89233e609..f05f651f6 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -3411,4 +3411,16 @@ c f + + + @string/femometer_measurement_mode_quick + @string/femometer_measurement_mode_normal + @string/femometer_measurement_mode_precise + + + quick + normal + precise + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 897cf8140..d39539a87 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -428,6 +428,11 @@ Normal Power saving Only watch + + Measurement mode + Quick Mode (30s) + Normal Mode (60s-90s) + Precise Mode (3min) Makibes HR3 settings @@ -1373,6 +1378,7 @@ Sony WF-1000XM5 Sony LinkBuds S Binary sensor + Femometer Vinca II Choose export location General High-priority diff --git a/app/src/main/res/xml/devicesettings_femometer.xml b/app/src/main/res/xml/devicesettings_femometer.xml new file mode 100644 index 000000000..1e41c4665 --- /dev/null +++ b/app/src/main/res/xml/devicesettings_femometer.xml @@ -0,0 +1,11 @@ + + + + \ No newline at end of file