mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge
synced 2024-12-23 09:07:44 +01:00
Garmin: Add intensity minutes
This commit is contained in:
parent
396aa41647
commit
199a6835a2
@ -54,7 +54,7 @@ public class GBDaoGenerator {
|
||||
|
||||
|
||||
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 user = addUserInfo(schema, userAttributes);
|
||||
@ -131,6 +131,7 @@ public class GBDaoGenerator {
|
||||
addGarminHeartRateRestingSample(schema, user, device);
|
||||
addGarminRestingMetabolicRateSample(schema, user, device);
|
||||
addGarminSleepStatsSample(schema, user, device);
|
||||
addGarminIntensityMinutesSample(schema, user, device);
|
||||
addPendingFile(schema, user, device);
|
||||
addWena3EnergySample(schema, user, device);
|
||||
addWena3BehaviorSample(schema, user, device);
|
||||
@ -813,6 +814,7 @@ public class GBDaoGenerator {
|
||||
addHeartRateProperties(activitySample);
|
||||
activitySample.addIntProperty("distanceCm").notNull().codeBeforeGetterAndSetter(OVERRIDE);
|
||||
activitySample.addIntProperty("activeCalories").notNull().codeBeforeGetterAndSetter(OVERRIDE);
|
||||
|
||||
return activitySample;
|
||||
}
|
||||
|
||||
@ -903,6 +905,14 @@ public class GBDaoGenerator {
|
||||
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) {
|
||||
Entity pendingFile = addEntity(schema, "PendingFile");
|
||||
pendingFile.setJavaDoc(
|
||||
|
@ -17,10 +17,12 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities.charts;
|
||||
|
||||
import android.graphics.Color;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.core.content.ContextCompat;
|
||||
@ -78,6 +80,9 @@ public class PaiChartFragment extends AbstractChartFragment<PaiChartFragment.Pai
|
||||
protected TextView mLineModerateTime;
|
||||
protected TextView mLineHighInc;
|
||||
protected TextView mLineHighTime;
|
||||
protected LinearLayout mTileLow;
|
||||
protected LinearLayout mTileModerate;
|
||||
protected LinearLayout mTileHigh;
|
||||
|
||||
protected int BACKGROUND_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);
|
||||
|
||||
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);
|
||||
mWeekChart = rootView.findViewById(R.id.pai_chart_week);
|
||||
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);
|
||||
mLineHighInc = rootView.findViewById(R.id.pai_line_high_inc);
|
||||
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()) {
|
||||
mLineLowTime.setVisibility(View.GONE);
|
||||
@ -123,6 +137,10 @@ public class PaiChartFragment extends AbstractChartFragment<PaiChartFragment.Pai
|
||||
mLineHighTime.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
if (!getChartsHost().getDevice().getDeviceCoordinator().supportsPaiLow()) {
|
||||
mTileLow.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
setupWeekChart();
|
||||
setupTodayPieChart();
|
||||
|
||||
@ -170,7 +188,7 @@ public class PaiChartFragment extends AbstractChartFragment<PaiChartFragment.Pai
|
||||
y.setDrawZeroLine(true);
|
||||
y.setSpaceBottom(0);
|
||||
y.setAxisMinimum(0);
|
||||
y.setAxisMaximum(100);
|
||||
y.setAxisMaximum(getPaiTarget());
|
||||
y.setValueFormatter(getRoundFormatter());
|
||||
y.setEnabled(true);
|
||||
|
||||
@ -190,6 +208,10 @@ public class PaiChartFragment extends AbstractChartFragment<PaiChartFragment.Pai
|
||||
}
|
||||
}
|
||||
|
||||
private int getPaiTarget() {
|
||||
return getChartsHost().getDevice().getDeviceCoordinator().getPaiTarget();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTitle() {
|
||||
if (GBApplication.getPrefs().getBoolean("charts_range", true)) {
|
||||
@ -257,7 +279,7 @@ public class PaiChartFragment extends AbstractChartFragment<PaiChartFragment.Pai
|
||||
Calendar day,
|
||||
final GBDevice device) {
|
||||
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<>();
|
||||
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.setValueTextSize(10f);
|
||||
|
||||
barChart.getAxisLeft().setAxisMaximum(Math.max(maxPai, 100));
|
||||
barChart.getAxisLeft().setAxisMaximum(Math.max(maxPai, getPaiTarget()));
|
||||
|
||||
//LimitLine target = new LimitLine(mTargetValue);
|
||||
//barChart.getAxisLeft().removeAllLimitLines();
|
||||
@ -364,7 +386,7 @@ public class PaiChartFragment extends AbstractChartFragment<PaiChartFragment.Pai
|
||||
final PieData data = new PieData();
|
||||
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) : "";
|
||||
|
||||
entries.add(new PieEntry(total - today, ""));
|
||||
|
@ -559,6 +559,16 @@ public abstract class AbstractDeviceCoordinator implements DeviceCoordinator {
|
||||
return supportsPai();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsPaiLow() {
|
||||
return supportsPai();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPaiTarget() {
|
||||
return 100;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsRespiratoryRate() {
|
||||
return false;
|
||||
|
@ -272,6 +272,16 @@ public interface DeviceCoordinator {
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
|
@ -44,6 +44,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.BodyEnergySample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.HrvSummarySample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.HrvValueSample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.PaiSample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.RespiratoryRateSample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.RestingMetabolicRateSample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.SleepScoreSample;
|
||||
@ -150,6 +151,11 @@ public abstract class GarminCoordinator extends AbstractBLEDeviceCoordinator {
|
||||
return new GarminSpo2SampleProvider(device, session);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TimeSampleProvider<? extends PaiSample> getPaiSampleProvider(final GBDevice device, final DaoSession session) {
|
||||
return new GarminPaiSampleProvider(device, session);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TimeSampleProvider<? extends RespiratoryRateSample> getRespiratoryRateSampleProvider(final GBDevice device, final DaoSession session) {
|
||||
return new GarminRespiratoryRateSampleProvider(device, session);
|
||||
@ -315,6 +321,32 @@ public abstract class GarminCoordinator extends AbstractBLEDeviceCoordinator {
|
||||
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
|
||||
public boolean supportsFindDevice() {
|
||||
return true;
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -26,6 +26,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminEventSampleProv
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminHeartRateRestingSampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminHrvSummarySampleProvider;
|
||||
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.GarminRestingMetabolicRateSampleProvider;
|
||||
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.GarminEventSample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.GarminHeartRateRestingSample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.GarminIntensityMinutesSample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.GarminRestingMetabolicRateSample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.GarminHrvSummarySample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.GarminHrvValueSample;
|
||||
@ -433,6 +435,7 @@ public class FitImporter {
|
||||
}
|
||||
|
||||
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
|
||||
// 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.setActiveCalories(ActivitySample.NOT_MEASURED);
|
||||
|
||||
boolean hasSteps = false;
|
||||
boolean hasDistance = false;
|
||||
boolean hasCalories = false;
|
||||
int minutesModerate = 0;
|
||||
int minutesVigorous = 0;
|
||||
|
||||
for (final FitMonitoring record : Objects.requireNonNull(records)) {
|
||||
final Integer activityType = record.getComputedActivityType().orElse(ActivitySample.NOT_MEASURED);
|
||||
|
||||
@ -486,41 +489,48 @@ public class FitImporter {
|
||||
final Long steps = record.getCycles();
|
||||
if (steps != null) {
|
||||
stepsPerActivity.put(activityType, steps);
|
||||
hasSteps = true;
|
||||
}
|
||||
|
||||
final Long distance = record.getDistance();
|
||||
if (distance != null) {
|
||||
distancePerActivity.put(activityType, distance);
|
||||
hasDistance = true;
|
||||
}
|
||||
|
||||
final Integer calories = record.getActiveCalories();
|
||||
if (calories != null) {
|
||||
caloriesPerActivity.put(activityType, calories);
|
||||
hasCalories = true;
|
||||
}
|
||||
|
||||
final Integer intensity = record.getComputedIntensity();
|
||||
if (intensity != null) {
|
||||
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;
|
||||
for (final Long steps : stepsPerActivity.values()) {
|
||||
sumSteps += steps;
|
||||
}
|
||||
sample.setSteps(sumSteps);
|
||||
}
|
||||
if (hasDistance) {
|
||||
if (!distancePerActivity.isEmpty()) {
|
||||
int sumDistance = 0;
|
||||
for (final Long distance : distancePerActivity.values()) {
|
||||
sumDistance += distance;
|
||||
}
|
||||
sample.setDistanceCm(sumDistance);
|
||||
}
|
||||
if (hasCalories) {
|
||||
if (!caloriesPerActivity.isEmpty()) {
|
||||
int sumCalories = 0;
|
||||
for (final Integer calories : caloriesPerActivity.values()) {
|
||||
sumCalories += calories;
|
||||
@ -530,6 +540,14 @@ public class FitImporter {
|
||||
|
||||
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();
|
||||
prevTs = (int) ts;
|
||||
}
|
||||
@ -551,6 +569,12 @@ public class FitImporter {
|
||||
} catch (final Exception 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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -208,6 +208,8 @@ public class GlobalFITMessage {
|
||||
new FieldDefinitionPrimitive(24, BaseType.BASE_TYPE_BYTE, "current_activity_type_intensity"),
|
||||
new FieldDefinitionPrimitive(26, BaseType.UINT16, "timestamp_16"),
|
||||
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)
|
||||
));
|
||||
|
||||
|
@ -68,6 +68,16 @@ public class FitMonitoring extends RecordData {
|
||||
return (Integer) getFieldByNumber(27);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Integer getModerateActivityMinutes() {
|
||||
return (Integer) getFieldByNumber(33);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Integer getVigorousActivityMinutes() {
|
||||
return (Integer) getFieldByNumber(34);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Long getTimestamp() {
|
||||
return (Long) getFieldByNumber(253);
|
||||
|
@ -93,6 +93,7 @@
|
||||
|
||||
<LinearLayout
|
||||
style="@style/GridTile"
|
||||
android:id="@+id/pai_tile_low"
|
||||
android:layout_marginTop="2dp"
|
||||
android:layout_marginEnd="1dp">
|
||||
|
||||
@ -120,6 +121,7 @@
|
||||
|
||||
<LinearLayout
|
||||
style="@style/GridTile"
|
||||
android:id="@+id/pai_tile_moderate"
|
||||
android:layout_marginStart="1dp"
|
||||
android:layout_marginTop="2dp"
|
||||
android:layout_marginEnd="1dp">
|
||||
@ -147,8 +149,8 @@
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/sleep_chart_legend_movement_intensity_wrapper"
|
||||
style="@style/GridTile"
|
||||
android:id="@+id/pai_tile_high"
|
||||
android:layout_marginStart="1dp"
|
||||
android:layout_marginTop="2dp"
|
||||
android:layout_marginBottom="2dp">
|
||||
|
@ -3220,6 +3220,7 @@
|
||||
<string name="devicetype_mi_watch_color_sport">Mi Watch Color Sport</string>
|
||||
<string name="devicetype_pixoo">Pixoo</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_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>
|
||||
|
Loading…
Reference in New Issue
Block a user