1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2024-12-23 17:17:46 +01:00

Garmin: Add intensity minutes

This commit is contained in:
José Rebelo 2024-12-15 21:22:29 +00:00
parent 396aa41647
commit 199a6835a2
12 changed files with 435 additions and 15 deletions

View File

@ -54,7 +54,7 @@ public class GBDaoGenerator {
public static void main(String[] args) throws Exception { public static void main(String[] args) throws Exception {
final Schema schema = new Schema(91, MAIN_PACKAGE + ".entities"); final Schema schema = new Schema(92, MAIN_PACKAGE + ".entities");
Entity userAttributes = addUserAttributes(schema); Entity userAttributes = addUserAttributes(schema);
Entity user = addUserInfo(schema, userAttributes); Entity user = addUserInfo(schema, userAttributes);
@ -131,6 +131,7 @@ public class GBDaoGenerator {
addGarminHeartRateRestingSample(schema, user, device); addGarminHeartRateRestingSample(schema, user, device);
addGarminRestingMetabolicRateSample(schema, user, device); addGarminRestingMetabolicRateSample(schema, user, device);
addGarminSleepStatsSample(schema, user, device); addGarminSleepStatsSample(schema, user, device);
addGarminIntensityMinutesSample(schema, user, device);
addPendingFile(schema, user, device); addPendingFile(schema, user, device);
addWena3EnergySample(schema, user, device); addWena3EnergySample(schema, user, device);
addWena3BehaviorSample(schema, user, device); addWena3BehaviorSample(schema, user, device);
@ -813,6 +814,7 @@ public class GBDaoGenerator {
addHeartRateProperties(activitySample); addHeartRateProperties(activitySample);
activitySample.addIntProperty("distanceCm").notNull().codeBeforeGetterAndSetter(OVERRIDE); activitySample.addIntProperty("distanceCm").notNull().codeBeforeGetterAndSetter(OVERRIDE);
activitySample.addIntProperty("activeCalories").notNull().codeBeforeGetterAndSetter(OVERRIDE); activitySample.addIntProperty("activeCalories").notNull().codeBeforeGetterAndSetter(OVERRIDE);
return activitySample; return activitySample;
} }
@ -903,6 +905,14 @@ public class GBDaoGenerator {
return sample; return sample;
} }
private static Entity addGarminIntensityMinutesSample(Schema schema, Entity user, Entity device) {
Entity sample = addEntity(schema, "GarminIntensityMinutesSample");
addCommonTimeSampleProperties("AbstractTimeSample", sample, user, device);
sample.addIntProperty("moderate");
sample.addIntProperty("vigorous");
return sample;
}
private static Entity addPendingFile(Schema schema, Entity user, Entity device) { private static Entity addPendingFile(Schema schema, Entity user, Entity device) {
Entity pendingFile = addEntity(schema, "PendingFile"); Entity pendingFile = addEntity(schema, "PendingFile");
pendingFile.setJavaDoc( pendingFile.setJavaDoc(

View File

@ -17,10 +17,12 @@
package nodomain.freeyourgadget.gadgetbridge.activities.charts; package nodomain.freeyourgadget.gadgetbridge.activities.charts;
import android.graphics.Color; import android.graphics.Color;
import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.TextView; import android.widget.TextView;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
@ -78,6 +80,9 @@ public class PaiChartFragment extends AbstractChartFragment<PaiChartFragment.Pai
protected TextView mLineModerateTime; protected TextView mLineModerateTime;
protected TextView mLineHighInc; protected TextView mLineHighInc;
protected TextView mLineHighTime; protected TextView mLineHighTime;
protected LinearLayout mTileLow;
protected LinearLayout mTileModerate;
protected LinearLayout mTileHigh;
protected int BACKGROUND_COLOR; protected int BACKGROUND_COLOR;
protected int DESCRIPTION_COLOR; protected int DESCRIPTION_COLOR;
@ -105,6 +110,12 @@ public class PaiChartFragment extends AbstractChartFragment<PaiChartFragment.Pai
final View rootView = inflater.inflate(R.layout.fragment_pai_chart, container, false); final View rootView = inflater.inflate(R.layout.fragment_pai_chart, container, false);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
rootView.setOnScrollChangeListener((v, scrollX, scrollY, oldScrollX, oldScrollY) -> {
getChartsHost().enableSwipeRefresh(scrollY == 0);
});
}
mTodayPieChart = rootView.findViewById(R.id.pai_chart_today); mTodayPieChart = rootView.findViewById(R.id.pai_chart_today);
mWeekChart = rootView.findViewById(R.id.pai_chart_week); mWeekChart = rootView.findViewById(R.id.pai_chart_week);
mDateView = rootView.findViewById(R.id.pai_date_view); mDateView = rootView.findViewById(R.id.pai_date_view);
@ -116,6 +127,9 @@ public class PaiChartFragment extends AbstractChartFragment<PaiChartFragment.Pai
mLineModerateTime = rootView.findViewById(R.id.pai_line_moderate_time); mLineModerateTime = rootView.findViewById(R.id.pai_line_moderate_time);
mLineHighInc = rootView.findViewById(R.id.pai_line_high_inc); mLineHighInc = rootView.findViewById(R.id.pai_line_high_inc);
mLineHighTime = rootView.findViewById(R.id.pai_line_high_time); mLineHighTime = rootView.findViewById(R.id.pai_line_high_time);
mTileLow = rootView.findViewById(R.id.pai_tile_low);
mTileModerate = rootView.findViewById(R.id.pai_tile_moderate);
mTileHigh = rootView.findViewById(R.id.pai_tile_high);
if (!getChartsHost().getDevice().getDeviceCoordinator().supportsPaiTime()) { if (!getChartsHost().getDevice().getDeviceCoordinator().supportsPaiTime()) {
mLineLowTime.setVisibility(View.GONE); mLineLowTime.setVisibility(View.GONE);
@ -123,6 +137,10 @@ public class PaiChartFragment extends AbstractChartFragment<PaiChartFragment.Pai
mLineHighTime.setVisibility(View.GONE); mLineHighTime.setVisibility(View.GONE);
} }
if (!getChartsHost().getDevice().getDeviceCoordinator().supportsPaiLow()) {
mTileLow.setVisibility(View.GONE);
}
setupWeekChart(); setupWeekChart();
setupTodayPieChart(); setupTodayPieChart();
@ -170,7 +188,7 @@ public class PaiChartFragment extends AbstractChartFragment<PaiChartFragment.Pai
y.setDrawZeroLine(true); y.setDrawZeroLine(true);
y.setSpaceBottom(0); y.setSpaceBottom(0);
y.setAxisMinimum(0); y.setAxisMinimum(0);
y.setAxisMaximum(100); y.setAxisMaximum(getPaiTarget());
y.setValueFormatter(getRoundFormatter()); y.setValueFormatter(getRoundFormatter());
y.setEnabled(true); y.setEnabled(true);
@ -190,6 +208,10 @@ public class PaiChartFragment extends AbstractChartFragment<PaiChartFragment.Pai
} }
} }
private int getPaiTarget() {
return getChartsHost().getDevice().getDeviceCoordinator().getPaiTarget();
}
@Override @Override
public String getTitle() { public String getTitle() {
if (GBApplication.getPrefs().getBoolean("charts_range", true)) { if (GBApplication.getPrefs().getBoolean("charts_range", true)) {
@ -257,7 +279,7 @@ public class PaiChartFragment extends AbstractChartFragment<PaiChartFragment.Pai
Calendar day, Calendar day,
final GBDevice device) { final GBDevice device) {
day = (Calendar) day.clone(); // do not modify the caller's argument day = (Calendar) day.clone(); // do not modify the caller's argument
day.add(Calendar.DATE, -TOTAL_DAYS); day.add(Calendar.DATE, -TOTAL_DAYS + 1);
List<BarEntry> entries = new ArrayList<>(); List<BarEntry> entries = new ArrayList<>();
final ArrayList<String> labels = new ArrayList<>(); final ArrayList<String> labels = new ArrayList<>();
@ -297,7 +319,7 @@ public class PaiChartFragment extends AbstractChartFragment<PaiChartFragment.Pai
barData.setValueTextColor(Color.GRAY); //prevent tearing other graph elements with the black text. Another approach would be to hide the values cmpletely with data.setDrawValues(false); barData.setValueTextColor(Color.GRAY); //prevent tearing other graph elements with the black text. Another approach would be to hide the values cmpletely with data.setDrawValues(false);
barData.setValueTextSize(10f); barData.setValueTextSize(10f);
barChart.getAxisLeft().setAxisMaximum(Math.max(maxPai, 100)); barChart.getAxisLeft().setAxisMaximum(Math.max(maxPai, getPaiTarget()));
//LimitLine target = new LimitLine(mTargetValue); //LimitLine target = new LimitLine(mTargetValue);
//barChart.getAxisLeft().removeAllLimitLines(); //barChart.getAxisLeft().removeAllLimitLines();
@ -364,7 +386,7 @@ public class PaiChartFragment extends AbstractChartFragment<PaiChartFragment.Pai
final PieData data = new PieData(); final PieData data = new PieData();
final List<PieEntry> entries = new ArrayList<>(); final List<PieEntry> entries = new ArrayList<>();
final int maxPai = Math.max(100, total); final int maxPai = Math.max(getPaiTarget(), total);
final String todayLabel = today != 0 ? requireContext().getString(R.string.pai_plus_num, today) : ""; final String todayLabel = today != 0 ? requireContext().getString(R.string.pai_plus_num, today) : "";
entries.add(new PieEntry(total - today, "")); entries.add(new PieEntry(total - today, ""));

View File

@ -559,6 +559,16 @@ public abstract class AbstractDeviceCoordinator implements DeviceCoordinator {
return supportsPai(); return supportsPai();
} }
@Override
public boolean supportsPaiLow() {
return supportsPai();
}
@Override
public int getPaiTarget() {
return 100;
}
@Override @Override
public boolean supportsRespiratoryRate() { public boolean supportsRespiratoryRate() {
return false; return false;

View File

@ -272,6 +272,16 @@ public interface DeviceCoordinator {
*/ */
boolean supportsPaiTime(); boolean supportsPaiTime();
/**
* Returns true if the device is capable of providing the time contribution for light PAI type.
*/
boolean supportsPaiLow();
/**
* Returns the PAI target - usually 100.
*/
int getPaiTarget();
/** /**
* Indicates whether the device supports respiratory rate tracking. * Indicates whether the device supports respiratory rate tracking.
*/ */

View File

@ -44,6 +44,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser;
import nodomain.freeyourgadget.gadgetbridge.model.BodyEnergySample; import nodomain.freeyourgadget.gadgetbridge.model.BodyEnergySample;
import nodomain.freeyourgadget.gadgetbridge.model.HrvSummarySample; import nodomain.freeyourgadget.gadgetbridge.model.HrvSummarySample;
import nodomain.freeyourgadget.gadgetbridge.model.HrvValueSample; import nodomain.freeyourgadget.gadgetbridge.model.HrvValueSample;
import nodomain.freeyourgadget.gadgetbridge.model.PaiSample;
import nodomain.freeyourgadget.gadgetbridge.model.RespiratoryRateSample; import nodomain.freeyourgadget.gadgetbridge.model.RespiratoryRateSample;
import nodomain.freeyourgadget.gadgetbridge.model.RestingMetabolicRateSample; import nodomain.freeyourgadget.gadgetbridge.model.RestingMetabolicRateSample;
import nodomain.freeyourgadget.gadgetbridge.model.SleepScoreSample; import nodomain.freeyourgadget.gadgetbridge.model.SleepScoreSample;
@ -150,6 +151,11 @@ public abstract class GarminCoordinator extends AbstractBLEDeviceCoordinator {
return new GarminSpo2SampleProvider(device, session); return new GarminSpo2SampleProvider(device, session);
} }
@Override
public TimeSampleProvider<? extends PaiSample> getPaiSampleProvider(final GBDevice device, final DaoSession session) {
return new GarminPaiSampleProvider(device, session);
}
@Override @Override
public TimeSampleProvider<? extends RespiratoryRateSample> getRespiratoryRateSampleProvider(final GBDevice device, final DaoSession session) { public TimeSampleProvider<? extends RespiratoryRateSample> getRespiratoryRateSampleProvider(final GBDevice device, final DaoSession session) {
return new GarminRespiratoryRateSampleProvider(device, session); return new GarminRespiratoryRateSampleProvider(device, session);
@ -315,6 +321,32 @@ public abstract class GarminCoordinator extends AbstractBLEDeviceCoordinator {
return true; return true;
} }
@Override
public boolean supportsPai() {
// Intensity Minutes
return true;
}
@Override
public int getPaiName() {
return R.string.garmin_intensity_minutes;
}
@Override
public boolean supportsPaiTime() {
return true;
}
@Override
public boolean supportsPaiLow() {
return false;
}
@Override
public int getPaiTarget() {
return 150;
}
@Override @Override
public boolean supportsFindDevice() { public boolean supportsFindDevice() {
return true; return true;

View File

@ -0,0 +1,56 @@
/* 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 <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.garmin;
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.GarminIntensityMinutesSample;
import nodomain.freeyourgadget.gadgetbridge.entities.GarminIntensityMinutesSampleDao;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
public class GarminIntensityMinutesSampleProvider extends AbstractTimeSampleProvider<GarminIntensityMinutesSample> {
public GarminIntensityMinutesSampleProvider(final GBDevice device, final DaoSession session) {
super(device, session);
}
@NonNull
@Override
public AbstractDao<GarminIntensityMinutesSample, ?> getSampleDao() {
return getSession().getGarminIntensityMinutesSampleDao();
}
@NonNull
@Override
protected Property getTimestampSampleProperty() {
return GarminIntensityMinutesSampleDao.Properties.Timestamp;
}
@NonNull
@Override
protected Property getDeviceIdentifierSampleProperty() {
return GarminIntensityMinutesSampleDao.Properties.DeviceId;
}
@Override
public GarminIntensityMinutesSample createSample() {
return new GarminIntensityMinutesSample();
}
}

View File

@ -0,0 +1,241 @@
/* 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 <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.garmin;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.time.DayOfWeek;
import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit;
import java.time.temporal.TemporalAdjusters;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.devices.TimeSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.GarminIntensityMinutesSample;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.PaiSample;
public class GarminPaiSampleProvider implements TimeSampleProvider<PaiSample> {
private final GarminIntensityMinutesSampleProvider intensityMinutesSampleProvider;
public GarminPaiSampleProvider(final GBDevice device, final DaoSession session) {
this.intensityMinutesSampleProvider = new GarminIntensityMinutesSampleProvider(device, session);
}
@NonNull
@Override
public List<PaiSample> getAllSamples(final long timestampFrom, final long timestampTo) {
// Intensity minutes reset every monday, so we need to go to the previous monday if we
// are not there yet
final ZoneId tz = ZoneId.systemDefault();
ZonedDateTime zonedDateTime = ZonedDateTime.ofInstant(Instant.ofEpochMilli(timestampFrom), tz);
final DayOfWeek dayOfWeek = zonedDateTime.getDayOfWeek();
if (dayOfWeek != DayOfWeek.MONDAY) {
zonedDateTime = zonedDateTime.with(TemporalAdjusters.previous(DayOfWeek.MONDAY));
}
zonedDateTime = zonedDateTime.truncatedTo(ChronoUnit.DAYS);
final List<GarminIntensityMinutesSample> allSamples = intensityMinutesSampleProvider.getAllSamples(
zonedDateTime.toEpochSecond() * 1000L,
timestampTo
);
if (allSamples.isEmpty()) {
return Collections.emptyList();
}
int totalModerate = 0;
int totalVigorous = 0;
int currentDay = 0;
LocalDate lastSampleDate = LocalDate.ofInstant(Instant.ofEpochMilli(allSamples.get(0).getTimestamp()), tz);
final List<PaiSample> ret = new ArrayList<>(allSamples.size());
for (final GarminIntensityMinutesSample sample : allSamples) {
final LocalDate sampleDate = LocalDate.ofInstant(Instant.ofEpochMilli(sample.getTimestamp()), tz);
// Since we only persist minute samples for days where there was activity, we need to fill any gaps
for (LocalDate d = lastSampleDate.plusDays(1); d.isBefore(sampleDate); d = d.plusDays(1)) {
if (lastSampleDate.getDayOfWeek() != d.getDayOfWeek()) {
currentDay = 0;
if (d.getDayOfWeek() == DayOfWeek.MONDAY) {
totalModerate = 0;
totalVigorous = 0;
}
}
ret.add(new GarminPaiSample(
d.atStartOfDay(tz).toInstant().toEpochMilli(),
totalModerate,
totalVigorous,
currentDay
));
lastSampleDate = d;
}
if (sampleDate.getDayOfWeek() != lastSampleDate.getDayOfWeek()) {
currentDay = 0;
if (sampleDate.getDayOfWeek() == DayOfWeek.MONDAY) {
totalModerate = 0;
totalVigorous = 0;
}
}
totalModerate += sample.getModerate();
totalVigorous += sample.getVigorous();
currentDay += sample.getModerate() + sample.getVigorous() * 2;
if (sample.getTimestamp() >= timestampFrom && sample.getTimestamp() <= timestampTo) {
ret.add(new GarminPaiSample(
sample.getTimestamp(),
totalModerate,
totalVigorous,
currentDay
));
}
lastSampleDate = sampleDate;
}
// Finally, fill out from the last sample to the end of the timestampTo
final LocalDate endDate = LocalDate.ofInstant(Instant.ofEpochMilli(timestampTo), tz);
for (LocalDate d = lastSampleDate.plusDays(1); !d.isAfter(endDate); d = d.plusDays(1)) {
if (lastSampleDate.getDayOfWeek() != d.getDayOfWeek()) {
currentDay = 0;
if (d.getDayOfWeek() == DayOfWeek.MONDAY) {
totalModerate = 0;
totalVigorous = 0;
}
}
ret.add(new GarminPaiSample(
d.atStartOfDay(tz).toInstant().toEpochMilli(),
totalModerate,
totalVigorous,
currentDay
));
lastSampleDate = d;
}
return ret;
}
@Override
public void addSample(final PaiSample timeSample) {
throw new UnsupportedOperationException("This sample provider is read-only!");
}
@Override
public void addSamples(final List<PaiSample> timeSamples) {
throw new UnsupportedOperationException("This sample provider is read-only!");
}
@Override
public PaiSample createSample() {
throw new UnsupportedOperationException("This sample provider is read-only!");
}
@Nullable
@Override
public PaiSample getLatestSample() {
// TODO
return null;
}
@Nullable
@Override
public PaiSample getLatestSample(long until) {
// TODO
return null;
}
@Nullable
@Override
public PaiSample getFirstSample() {
// TODO
return null;
}
public static class GarminPaiSample implements PaiSample {
private final long timestamp;
private final int minutesModerate;
private final int minutesVigorous;
private final int today;
public GarminPaiSample(final long timestamp,
final int moderate,
final int vigorous,
final int today) {
this.timestamp = timestamp;
this.minutesModerate = moderate;
this.minutesVigorous = vigorous;
this.today = today;
}
@Override
public long getTimestamp() {
return timestamp;
}
@Override
public float getPaiLow() {
return 0;
}
@Override
public float getPaiModerate() {
return minutesModerate;
}
@Override
public float getPaiHigh() {
return minutesVigorous * 2;
}
@Override
public int getTimeLow() {
return 0; // not supported
}
@Override
public int getTimeModerate() {
return minutesModerate;
}
@Override
public int getTimeHigh() {
return minutesVigorous;
}
@Override
public float getPaiToday() {
return today;
}
@Override
public float getPaiTotal() {
return minutesModerate + 2 * minutesVigorous;
}
}
}

View File

@ -26,6 +26,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminEventSampleProv
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminHeartRateRestingSampleProvider; import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminHeartRateRestingSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminHrvSummarySampleProvider; import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminHrvSummarySampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminHrvValueSampleProvider; import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminHrvValueSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminIntensityMinutesSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminRespiratoryRateSampleProvider; import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminRespiratoryRateSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminRestingMetabolicRateSampleProvider; import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminRestingMetabolicRateSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminSleepStatsSampleProvider; import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminSleepStatsSampleProvider;
@ -41,6 +42,7 @@ import nodomain.freeyourgadget.gadgetbridge.entities.GarminActivitySample;
import nodomain.freeyourgadget.gadgetbridge.entities.GarminBodyEnergySample; import nodomain.freeyourgadget.gadgetbridge.entities.GarminBodyEnergySample;
import nodomain.freeyourgadget.gadgetbridge.entities.GarminEventSample; import nodomain.freeyourgadget.gadgetbridge.entities.GarminEventSample;
import nodomain.freeyourgadget.gadgetbridge.entities.GarminHeartRateRestingSample; import nodomain.freeyourgadget.gadgetbridge.entities.GarminHeartRateRestingSample;
import nodomain.freeyourgadget.gadgetbridge.entities.GarminIntensityMinutesSample;
import nodomain.freeyourgadget.gadgetbridge.entities.GarminRestingMetabolicRateSample; import nodomain.freeyourgadget.gadgetbridge.entities.GarminRestingMetabolicRateSample;
import nodomain.freeyourgadget.gadgetbridge.entities.GarminHrvSummarySample; import nodomain.freeyourgadget.gadgetbridge.entities.GarminHrvSummarySample;
import nodomain.freeyourgadget.gadgetbridge.entities.GarminHrvValueSample; import nodomain.freeyourgadget.gadgetbridge.entities.GarminHrvValueSample;
@ -433,6 +435,7 @@ public class FitImporter {
} }
final List<GarminActivitySample> activitySamples = new ArrayList<>(activitySamplesPerTimestamp.size()); final List<GarminActivitySample> activitySamples = new ArrayList<>(activitySamplesPerTimestamp.size());
final List<GarminIntensityMinutesSample> intensityMinutesSamples = new ArrayList<>(activitySamplesPerTimestamp.size());
// Garmin reports the cumulative data per activity, but not always, so we need to keep // Garmin reports the cumulative data per activity, but not always, so we need to keep
// track of the amounts for each activity, and set the sum of all on the sample // track of the amounts for each activity, and set the sum of all on the sample
@ -472,9 +475,9 @@ public class FitImporter {
sample.setDistanceCm(ActivitySample.NOT_MEASURED); sample.setDistanceCm(ActivitySample.NOT_MEASURED);
sample.setActiveCalories(ActivitySample.NOT_MEASURED); sample.setActiveCalories(ActivitySample.NOT_MEASURED);
boolean hasSteps = false; int minutesModerate = 0;
boolean hasDistance = false; int minutesVigorous = 0;
boolean hasCalories = false;
for (final FitMonitoring record : Objects.requireNonNull(records)) { for (final FitMonitoring record : Objects.requireNonNull(records)) {
final Integer activityType = record.getComputedActivityType().orElse(ActivitySample.NOT_MEASURED); final Integer activityType = record.getComputedActivityType().orElse(ActivitySample.NOT_MEASURED);
@ -486,41 +489,48 @@ public class FitImporter {
final Long steps = record.getCycles(); final Long steps = record.getCycles();
if (steps != null) { if (steps != null) {
stepsPerActivity.put(activityType, steps); stepsPerActivity.put(activityType, steps);
hasSteps = true;
} }
final Long distance = record.getDistance(); final Long distance = record.getDistance();
if (distance != null) { if (distance != null) {
distancePerActivity.put(activityType, distance); distancePerActivity.put(activityType, distance);
hasDistance = true;
} }
final Integer calories = record.getActiveCalories(); final Integer calories = record.getActiveCalories();
if (calories != null) { if (calories != null) {
caloriesPerActivity.put(activityType, calories); caloriesPerActivity.put(activityType, calories);
hasCalories = true;
} }
final Integer intensity = record.getComputedIntensity(); final Integer intensity = record.getComputedIntensity();
if (intensity != null) { if (intensity != null) {
sample.setRawIntensity(intensity); sample.setRawIntensity(intensity);
} }
final Integer recordMinutesModerate = record.getModerateActivityMinutes();
if (recordMinutesModerate != null) {
minutesModerate += recordMinutesModerate;
} }
if (hasSteps) {
final Integer recordMinutesVigorous = record.getVigorousActivityMinutes();
if (recordMinutesVigorous != null) {
minutesVigorous += recordMinutesVigorous;
}
}
if (!stepsPerActivity.isEmpty()) {
int sumSteps = 0; int sumSteps = 0;
for (final Long steps : stepsPerActivity.values()) { for (final Long steps : stepsPerActivity.values()) {
sumSteps += steps; sumSteps += steps;
} }
sample.setSteps(sumSteps); sample.setSteps(sumSteps);
} }
if (hasDistance) { if (!distancePerActivity.isEmpty()) {
int sumDistance = 0; int sumDistance = 0;
for (final Long distance : distancePerActivity.values()) { for (final Long distance : distancePerActivity.values()) {
sumDistance += distance; sumDistance += distance;
} }
sample.setDistanceCm(sumDistance); sample.setDistanceCm(sumDistance);
} }
if (hasCalories) { if (!caloriesPerActivity.isEmpty()) {
int sumCalories = 0; int sumCalories = 0;
for (final Integer calories : caloriesPerActivity.values()) { for (final Integer calories : caloriesPerActivity.values()) {
sumCalories += calories; sumCalories += calories;
@ -530,6 +540,14 @@ public class FitImporter {
activitySamples.add(sample); activitySamples.add(sample);
if (minutesModerate != 0 || minutesVigorous != 0) {
final GarminIntensityMinutesSample intensityMinutesSample = new GarminIntensityMinutesSample();
intensityMinutesSample.setTimestamp(ts * 1000L);
intensityMinutesSample.setModerate(minutesModerate);
intensityMinutesSample.setVigorous(minutesVigorous);
intensityMinutesSamples.add(intensityMinutesSample);
}
prevActivityKind = sample.getRawKind(); prevActivityKind = sample.getRawKind();
prevTs = (int) ts; prevTs = (int) ts;
} }
@ -551,6 +569,12 @@ public class FitImporter {
} catch (final Exception e) { } catch (final Exception e) {
GB.toast(context, "Error saving activity samples", Toast.LENGTH_LONG, GB.ERROR, e); GB.toast(context, "Error saving activity samples", Toast.LENGTH_LONG, GB.ERROR, e);
} }
try {
persistAbstractSamples(intensityMinutesSamples, new GarminIntensityMinutesSampleProvider(gbDevice, session));
} catch (final Exception e) {
GB.toast(context, "Error saving intensity minutes samples", Toast.LENGTH_LONG, GB.ERROR, e);
}
} }
/** /**

View File

@ -208,6 +208,8 @@ public class GlobalFITMessage {
new FieldDefinitionPrimitive(24, BaseType.BASE_TYPE_BYTE, "current_activity_type_intensity"), new FieldDefinitionPrimitive(24, BaseType.BASE_TYPE_BYTE, "current_activity_type_intensity"),
new FieldDefinitionPrimitive(26, BaseType.UINT16, "timestamp_16"), new FieldDefinitionPrimitive(26, BaseType.UINT16, "timestamp_16"),
new FieldDefinitionPrimitive(27, BaseType.UINT8, "heart_rate"), new FieldDefinitionPrimitive(27, BaseType.UINT8, "heart_rate"),
new FieldDefinitionPrimitive(33, BaseType.UINT16, "moderate_activity_minutes"),
new FieldDefinitionPrimitive(34, BaseType.UINT16, "vigorous_activity_minutes"),
new FieldDefinitionPrimitive(253, BaseType.UINT32, "timestamp", FieldDefinitionFactory.FIELD.TIMESTAMP) new FieldDefinitionPrimitive(253, BaseType.UINT32, "timestamp", FieldDefinitionFactory.FIELD.TIMESTAMP)
)); ));

View File

@ -68,6 +68,16 @@ public class FitMonitoring extends RecordData {
return (Integer) getFieldByNumber(27); return (Integer) getFieldByNumber(27);
} }
@Nullable
public Integer getModerateActivityMinutes() {
return (Integer) getFieldByNumber(33);
}
@Nullable
public Integer getVigorousActivityMinutes() {
return (Integer) getFieldByNumber(34);
}
@Nullable @Nullable
public Long getTimestamp() { public Long getTimestamp() {
return (Long) getFieldByNumber(253); return (Long) getFieldByNumber(253);

View File

@ -93,6 +93,7 @@
<LinearLayout <LinearLayout
style="@style/GridTile" style="@style/GridTile"
android:id="@+id/pai_tile_low"
android:layout_marginTop="2dp" android:layout_marginTop="2dp"
android:layout_marginEnd="1dp"> android:layout_marginEnd="1dp">
@ -120,6 +121,7 @@
<LinearLayout <LinearLayout
style="@style/GridTile" style="@style/GridTile"
android:id="@+id/pai_tile_moderate"
android:layout_marginStart="1dp" android:layout_marginStart="1dp"
android:layout_marginTop="2dp" android:layout_marginTop="2dp"
android:layout_marginEnd="1dp"> android:layout_marginEnd="1dp">
@ -147,8 +149,8 @@
</LinearLayout> </LinearLayout>
<LinearLayout <LinearLayout
android:id="@+id/sleep_chart_legend_movement_intensity_wrapper"
style="@style/GridTile" style="@style/GridTile"
android:id="@+id/pai_tile_high"
android:layout_marginStart="1dp" android:layout_marginStart="1dp"
android:layout_marginTop="2dp" android:layout_marginTop="2dp"
android:layout_marginBottom="2dp"> android:layout_marginBottom="2dp">

View File

@ -3220,6 +3220,7 @@
<string name="devicetype_mi_watch_color_sport">Mi Watch Color Sport</string> <string name="devicetype_mi_watch_color_sport">Mi Watch Color Sport</string>
<string name="devicetype_pixoo">Pixoo</string> <string name="devicetype_pixoo">Pixoo</string>
<string name="not_set">Not set</string> <string name="not_set">Not set</string>
<string name="garmin_intensity_minutes">Intensity Minutes</string>
<string name="pref_vitality_score_title">Vitality Score</string> <string name="pref_vitality_score_title">Vitality Score</string>
<string name="pref_vitality_score_7_day_title">7-day progress</string> <string name="pref_vitality_score_7_day_title">7-day progress</string>
<string name="pref_vitality_score_7_day_summary">Get a notification when your vitality score reaches 30, 60 or 100 in the past 7 days</string> <string name="pref_vitality_score_7_day_summary">Get a notification when your vitality score reaches 30, 60 or 100 in the past 7 days</string>