/* 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.cmfwatchpro.samples; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.time.LocalDate; import java.util.ArrayList; import java.util.Calendar; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import de.greenrobot.dao.AbstractDao; import de.greenrobot.dao.Property; import nodomain.freeyourgadget.gadgetbridge.devices.AbstractSampleProvider; import nodomain.freeyourgadget.gadgetbridge.entities.CmfActivitySample; import nodomain.freeyourgadget.gadgetbridge.entities.CmfActivitySampleDao; import nodomain.freeyourgadget.gadgetbridge.entities.CmfHeartRateSample; import nodomain.freeyourgadget.gadgetbridge.entities.CmfSleepStageSample; import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind; import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample; public class CmfActivitySampleProvider extends AbstractSampleProvider { private static final Logger LOG = LoggerFactory.getLogger(CmfActivitySampleProvider.class); public CmfActivitySampleProvider(final GBDevice device, final DaoSession session) { super(device, session); } @Override public AbstractDao getSampleDao() { return getSession().getCmfActivitySampleDao(); } @Nullable @Override protected Property getRawKindSampleProperty() { return CmfActivitySampleDao.Properties.RawKind; } @NonNull @Override protected Property getTimestampSampleProperty() { return CmfActivitySampleDao.Properties.Timestamp; } @NonNull @Override protected Property getDeviceIdentifierSampleProperty() { return CmfActivitySampleDao.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 CmfActivitySample createActivitySample() { return new CmfActivitySample(); } @Override protected List getGBActivitySamples(final int timestamp_from, final int timestamp_to) { LOG.trace( "Getting cmf activity samples between {} and {}", timestamp_from, timestamp_to ); final long nanoStart = System.nanoTime(); final List samples = super.getGBActivitySamples(timestamp_from, timestamp_to); if (!samples.isEmpty()) { convertCumulativeSteps(samples, CmfActivitySampleDao.Properties.Steps); } final Map sampleByTs = new HashMap<>(); for (final CmfActivitySample sample : samples) { sampleByTs.put(sample.getTimestamp(), sample); } overlayHeartRate(sampleByTs, timestamp_from, timestamp_to); overlaySleep(sampleByTs, timestamp_from, timestamp_to); final List finalSamples = new ArrayList<>(sampleByTs.values()); Collections.sort(finalSamples, (a, b) -> Integer.compare(a.getTimestamp(), b.getTimestamp())); final long nanoEnd = System.nanoTime(); final long executionTime = (nanoEnd - nanoStart) / 1000000; LOG.trace("Getting cmf samples took {}ms", executionTime); return finalSamples; } private void overlayHeartRate(final Map sampleByTs, final int timestamp_from, final int timestamp_to) { final CmfHeartRateSampleProvider heartRateSampleProvider = new CmfHeartRateSampleProvider(getDevice(), getSession()); final List hrSamples = heartRateSampleProvider.getAllSamples(timestamp_from * 1000L, timestamp_to * 1000L); for (final CmfHeartRateSample hrSample : hrSamples) { // round to the nearest minute, we don't need per-second granularity final int tsSeconds = (int) ((hrSample.getTimestamp() / 1000) / 60) * 60; CmfActivitySample sample = sampleByTs.get(tsSeconds); if (sample == null) { //LOG.debug("Adding dummy sample at {} for hr", tsSeconds); sample = new CmfActivitySample(); sample.setTimestamp(tsSeconds); sample.setProvider(this); sampleByTs.put(tsSeconds, sample); } sample.setHeartRate(hrSample.getHeartRate()); } } private void overlaySleep(final Map sampleByTs, final int timestamp_from, final int timestamp_to) { final CmfSleepStageSampleProvider sleepStageSampleProvider = new CmfSleepStageSampleProvider(getDevice(), getSession()); final List sleepStageSamples = sleepStageSampleProvider.getAllSamples(timestamp_from * 1000L, timestamp_to * 1000L); for (final CmfSleepStageSample sleepStageSample : sleepStageSamples) { // round to the nearest minute, we don't need per-second granularity final int tsSeconds = (int) ((sleepStageSample.getTimestamp() / 1000) / 60) * 60; for (int i = tsSeconds; i < tsSeconds + sleepStageSample.getDuration(); i += 60) { CmfActivitySample sample = sampleByTs.get(i); if (sample == null) { //LOG.debug("Adding dummy sample at {} for sleep", i); sample = new CmfActivitySample(); sample.setTimestamp(i); sample.setProvider(this); sampleByTs.put(i, sample); } final ActivityKind sleepRawKind = sleepStageToActivityKind(sleepStageSample.getStage()); sample.setRawKind(sleepRawKind.getCode()); sample.setRawIntensity(ActivitySample.NOT_MEASURED); } } } final ActivityKind sleepStageToActivityKind(final int sleepStage) { switch (sleepStage) { case 1: return ActivityKind.DEEP_SLEEP; case 2: return ActivityKind.LIGHT_SLEEP; case 3: return ActivityKind.REM_SLEEP; default: return ActivityKind.UNKNOWN; } } }