/* Copyright (C) 2024 José Rebelo 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.garmin; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.List; import de.greenrobot.dao.AbstractDao; import de.greenrobot.dao.Property; import nodomain.freeyourgadget.gadgetbridge.devices.AbstractSampleProvider; import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; import nodomain.freeyourgadget.gadgetbridge.entities.GarminActivitySample; import nodomain.freeyourgadget.gadgetbridge.entities.GarminActivitySampleDao; import nodomain.freeyourgadget.gadgetbridge.entities.GarminEventSample; import nodomain.freeyourgadget.gadgetbridge.entities.GarminSleepStageSample; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind; import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionSleepStage; import nodomain.freeyourgadget.gadgetbridge.util.RangeMap; public class GarminActivitySampleProvider extends AbstractSampleProvider { private static final Logger LOG = LoggerFactory.getLogger(GarminActivitySampleProvider.class); public GarminActivitySampleProvider(final GBDevice device, final DaoSession session) { super(device, session); } @Override public AbstractDao getSampleDao() { return getSession().getGarminActivitySampleDao(); } @Nullable @Override protected Property getRawKindSampleProperty() { return GarminActivitySampleDao.Properties.RawKind; } @NonNull @Override protected Property getTimestampSampleProperty() { return GarminActivitySampleDao.Properties.Timestamp; } @NonNull @Override protected Property getDeviceIdentifierSampleProperty() { return GarminActivitySampleDao.Properties.DeviceId; } @Override public ActivityKind normalizeType(final int rawType) { return ActivityKind.fromCode(rawType); } @Override public int toRawActivityKind(final ActivityKind activityKind) { return activityKind.getCode(); } @Override public float normalizeIntensity(final int rawIntensity) { return rawIntensity / 100f; } @Override public GarminActivitySample createActivitySample() { return new GarminActivitySample(); } @Override protected List getGBActivitySamples(final int timestamp_from, final int timestamp_to) { LOG.trace( "Getting garmin activity samples between {} and {}", timestamp_from, timestamp_to ); final long nanoStart = System.nanoTime(); final List samples = fillGaps( super.getGBActivitySamples(timestamp_from, timestamp_to), timestamp_from, timestamp_to ); if (!samples.isEmpty()) { convertCumulativeSteps(samples, GarminActivitySampleDao.Properties.Steps); } convertCalories(samples); overlaySleep(samples, timestamp_from, timestamp_to); final long nanoEnd = System.nanoTime(); final long executionTime = (nanoEnd - nanoStart) / 1000000; LOG.trace("Getting Garmin samples took {}ms", executionTime); return samples; } /** * Converts the calories from kcal to cal */ private void convertCalories(List samples) { for (GarminActivitySample sample : samples) { sample.setActiveCalories(sample.getActiveCalories() * 1000); } } public void overlaySleep(final List samples, final int timestamp_from, final int timestamp_to) { // The samples provided by Garmin are upper-bound timestamps of the sleep stage final RangeMap stagesMap = new RangeMap<>(RangeMap.Mode.UPPER_BOUND); final GarminEventSampleProvider eventSampleProvider = new GarminEventSampleProvider(getDevice(), getSession()); final List sleepEventSamples = eventSampleProvider.getSleepEvents( timestamp_from * 1000L, timestamp_to * 1000L ); if (!sleepEventSamples.isEmpty()) { LOG.debug("Found {} sleep event samples between {} and {}", sleepEventSamples.size(), timestamp_from, timestamp_to); for (final GarminEventSample event : sleepEventSamples) { switch (event.getEventType()) { case 0: // start // We only need the start event as an upper-bound timestamp (anything before it is unknown) stagesMap.put(event.getTimestamp(), ActivityKind.UNKNOWN); break; case 1: // stop // See FitImporter#processRawSleepSamples / #4048 if (event.getData() != null && event.getData() == -1) { stagesMap.put(event.getTimestamp(), ActivityKind.LIGHT_SLEEP); } default: } } } final GarminSleepStageSampleProvider sleepStagesSampleProvider = new GarminSleepStageSampleProvider(getDevice(), getSession()); // Retrieve the next stage after this time range final GarminEventSample nextSleepStageAfterRange = eventSampleProvider.getNextSleepEventAfter(timestamp_to * 1000L); if (nextSleepStageAfterRange != null && nextSleepStageAfterRange.getEventType() == 1) { // Sleep session actually ends outside of this range, we need to fetch the next sleep stage final GarminSleepStageSample nextStage = sleepStagesSampleProvider.getNextSampleAfter(timestamp_to * 1000L); if (nextStage != null) { stagesMap.put(nextStage.getTimestamp(), toActivityKind(nextStage)); } } final List stageSamples = sleepStagesSampleProvider.getAllSamples( timestamp_from * 1000L, timestamp_to * 1000L ); if (!stageSamples.isEmpty()) { // We got actual sleep stages LOG.debug("Found {} sleep stage samples between {} and {}", stageSamples.size(), timestamp_from, timestamp_to); for (final GarminSleepStageSample stageSample : stageSamples) { stagesMap.put(stageSample.getTimestamp(), toActivityKind(stageSample)); } } if (!stagesMap.isEmpty()) { for (final GarminActivitySample sample : samples) { final long ts = sample.getTimestamp() * 1000L; final ActivityKind sleepType = stagesMap.get(ts); if (sleepType != null && !sleepType.equals(ActivityKind.UNKNOWN)) { sample.setRawKind(sleepType.getCode()); sample.setRawIntensity(ActivitySample.NOT_MEASURED); } } } } private ActivityKind toActivityKind(final GarminSleepStageSample stageSample) { final FieldDefinitionSleepStage.SleepStage sleepStage = FieldDefinitionSleepStage.SleepStage.fromId(stageSample.getStage()); if (sleepStage == null) { LOG.error("Unknown sleep stage for {}", stageSample.getStage()); return ActivityKind.UNKNOWN; } switch (sleepStage) { case AWAKE: return ActivityKind.AWAKE_SLEEP; case LIGHT: return ActivityKind.LIGHT_SLEEP; case DEEP: return ActivityKind.DEEP_SLEEP; case REM: return ActivityKind.REM_SLEEP; } return ActivityKind.UNKNOWN; } }