From 7b76d212541e45896230913dcf1cf2bf540a1fa6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Rebelo?= Date: Wed, 18 Sep 2024 21:08:50 +0100 Subject: [PATCH] Zepp OS: Add VO2 Max support --- ....java => WorkoutVo2MaxSampleProvider.java} | 73 +++++++++++++------ .../devices/garmin/GarminCoordinator.java | 3 +- .../huami/zeppos/ZeppOsCoordinator.java | 18 +++++ 3 files changed, 70 insertions(+), 24 deletions(-) rename app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/{garmin/GarminVo2MaxSampleProvider.java => WorkoutVo2MaxSampleProvider.java} (74%) diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/GarminVo2MaxSampleProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/WorkoutVo2MaxSampleProvider.java similarity index 74% rename from app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/GarminVo2MaxSampleProvider.java rename to app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/WorkoutVo2MaxSampleProvider.java index 598553683..466274c89 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/GarminVo2MaxSampleProvider.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/WorkoutVo2MaxSampleProvider.java @@ -14,7 +14,7 @@ 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.garmin; +package nodomain.freeyourgadget.gadgetbridge.devices; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -26,6 +26,7 @@ import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.List; @@ -33,8 +34,8 @@ import java.util.Objects; import java.util.stream.Collectors; import de.greenrobot.dao.query.QueryBuilder; +import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.database.DBHelper; -import nodomain.freeyourgadget.gadgetbridge.devices.Vo2MaxSampleProvider; import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary; import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummaryDao; import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; @@ -42,15 +43,20 @@ import nodomain.freeyourgadget.gadgetbridge.entities.Device; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind; import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries; +import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser; import nodomain.freeyourgadget.gadgetbridge.model.Vo2MaxSample; -public class GarminVo2MaxSampleProvider implements Vo2MaxSampleProvider { - private static final Logger LOG = LoggerFactory.getLogger(GarminVo2MaxSampleProvider.class); +/** + * An implementation of {@link Vo2MaxSampleProvider} that extracts VO2 Max values from + * workouts, for devices that provide them as part of the workout data. + */ +public class WorkoutVo2MaxSampleProvider implements Vo2MaxSampleProvider { + private static final Logger LOG = LoggerFactory.getLogger(WorkoutVo2MaxSampleProvider.class); private final GBDevice device; private final DaoSession session; - public GarminVo2MaxSampleProvider(final GBDevice device, final DaoSession session) { + public WorkoutVo2MaxSampleProvider(final GBDevice device, final DaoSession session) { this.device = device; this.session = session; } @@ -65,6 +71,8 @@ public class GarminVo2MaxSampleProvider implements Vo2MaxSampleProvider qb = summaryDao.queryBuilder(); qb.where(BaseActivitySummaryDao.Properties.DeviceId.eq(dbDevice.getId())) .where(BaseActivitySummaryDao.Properties.StartTime.gt(new Date(timestampFrom))) @@ -73,6 +81,7 @@ public class GarminVo2MaxSampleProvider implements Vo2MaxSampleProvider samples = qb.build().list(); summaryDao.detachAll(); + fillSummaryData(coordinator, samples); return samples.stream() .map(GarminVo2maxSample::fromActivitySummary) @@ -105,9 +114,10 @@ public class GarminVo2MaxSampleProvider implements Vo2MaxSampleProvider qb = summaryDao.queryBuilder(); - addWhereFilter(qb, type); + addWhereFilter(coordinator, qb, type); if (until != 0) { qb.where(BaseActivitySummaryDao.Properties.StartTime.le(new Date(until))); @@ -119,35 +129,52 @@ public class GarminVo2MaxSampleProvider implements Vo2MaxSampleProvider samples = qb.build().list(); summaryDao.detachAll(); + fillSummaryData(coordinator, samples); return !samples.isEmpty() ? GarminVo2maxSample.fromActivitySummary(samples.get(0)) : null; } - private static void addWhereFilter(final QueryBuilder qb, final Vo2MaxSample.Type type) { + private static void addWhereFilter(final DeviceCoordinator coordinator, + final QueryBuilder qb, + final Vo2MaxSample.Type type) { final List codes = new ArrayList<>(); - if (type == Vo2MaxSample.Type.ANY || type == Vo2MaxSample.Type.RUNNING) { - codes.addAll(Arrays.asList( - ActivityKind.INDOOR_RUNNING.getCode(), - ActivityKind.OUTDOOR_RUNNING.getCode(), - ActivityKind.CROSS_COUNTRY_RUNNING.getCode(), - ActivityKind.RUNNING.getCode() - )); + if (coordinator.supportsVO2MaxRunning()) { + if (type == Vo2MaxSample.Type.ANY || type == Vo2MaxSample.Type.RUNNING) { + codes.addAll(Arrays.asList( + ActivityKind.INDOOR_RUNNING.getCode(), + ActivityKind.OUTDOOR_RUNNING.getCode(), + ActivityKind.CROSS_COUNTRY_RUNNING.getCode(), + ActivityKind.RUNNING.getCode() + )); + } } - if (type == Vo2MaxSample.Type.ANY || type == Vo2MaxSample.Type.CYCLING) { - codes.addAll(Arrays.asList( - ActivityKind.CYCLING.getCode(), - ActivityKind.INDOOR_CYCLING.getCode(), - ActivityKind.HANDCYCLING.getCode(), - ActivityKind.HANDCYCLING_INDOOR.getCode(), - ActivityKind.MOTORCYCLING.getCode(), - ActivityKind.OUTDOOR_CYCLING.getCode() - )); + if (coordinator.supportsVO2MaxCycling()) { + if (type == Vo2MaxSample.Type.ANY || type == Vo2MaxSample.Type.CYCLING) { + codes.addAll(Arrays.asList( + ActivityKind.CYCLING.getCode(), + ActivityKind.INDOOR_CYCLING.getCode(), + ActivityKind.HANDCYCLING.getCode(), + ActivityKind.HANDCYCLING_INDOOR.getCode(), + ActivityKind.MOTORCYCLING.getCode(), + ActivityKind.OUTDOOR_CYCLING.getCode() + )); + } } qb.where(BaseActivitySummaryDao.Properties.ActivityKind.in(codes)); } + private void fillSummaryData(final DeviceCoordinator coordinator, + final Collection summaries) { + ActivitySummaryParser activitySummaryParser = coordinator.getActivitySummaryParser(device, GBApplication.getContext()); + for (final BaseActivitySummary summary : summaries) { + if (summary.getSummaryData() == null) { + activitySummaryParser.parseBinaryData(summary, true); + } + } + } + @Nullable @Override public Vo2MaxSample getLatestSample() { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/GarminCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/GarminCoordinator.java index ff472bdae..e156c418e 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/GarminCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/GarminCoordinator.java @@ -20,6 +20,7 @@ import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpec import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsCustomizer; import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsScreen; import nodomain.freeyourgadget.gadgetbridge.devices.AbstractBLEDeviceCoordinator; +import nodomain.freeyourgadget.gadgetbridge.devices.WorkoutVo2MaxSampleProvider; import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler; import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider; import nodomain.freeyourgadget.gadgetbridge.devices.TimeSampleProvider; @@ -138,7 +139,7 @@ public abstract class GarminCoordinator extends AbstractBLEDeviceCoordinator { @Override public Vo2MaxSampleProvider getVo2MaxSampleProvider(final GBDevice device, final DaoSession session) { - return new GarminVo2MaxSampleProvider(device, session); + return new WorkoutVo2MaxSampleProvider(device, session); } @Override diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/zeppos/ZeppOsCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/zeppos/ZeppOsCoordinator.java index a76ef7a64..a8ae35d13 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/zeppos/ZeppOsCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/zeppos/ZeppOsCoordinator.java @@ -43,6 +43,8 @@ import nodomain.freeyourgadget.gadgetbridge.capabilities.password.PasswordCapabi import nodomain.freeyourgadget.gadgetbridge.GBException; import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler; import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider; +import nodomain.freeyourgadget.gadgetbridge.devices.Vo2MaxSampleProvider; +import nodomain.freeyourgadget.gadgetbridge.devices.WorkoutVo2MaxSampleProvider; import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiExtendedSampleProvider; import nodomain.freeyourgadget.gadgetbridge.entities.AbstractActivitySample; @@ -58,6 +60,7 @@ import nodomain.freeyourgadget.gadgetbridge.entities.HuamiSpo2SampleDao; import nodomain.freeyourgadget.gadgetbridge.entities.HuamiStressSampleDao; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser; +import nodomain.freeyourgadget.gadgetbridge.model.Vo2MaxSample; import nodomain.freeyourgadget.gadgetbridge.service.DeviceSupport; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.ZeppOsFwInstallHandler; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.ZeppOsSupport; @@ -177,6 +180,16 @@ public abstract class ZeppOsCoordinator extends HuamiCoordinator { return true; } + @Override + public boolean supportsVO2Max() { + return true; + } + + @Override + public boolean supportsVO2MaxRunning() { + return true; + } + @Override public boolean supportsHeartRateStats() { return true; @@ -301,6 +314,11 @@ public abstract class ZeppOsCoordinator extends HuamiCoordinator { return new HuamiExtendedSampleProvider(device, session); } + @Override + public Vo2MaxSampleProvider getVo2MaxSampleProvider(final GBDevice device, final DaoSession session) { + return new WorkoutVo2MaxSampleProvider(device, session); + } + @Override public ActivitySummaryParser getActivitySummaryParser(final GBDevice device, final Context context) { return new ZeppOsActivitySummaryParser();