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:
parent
396aa41647
commit
199a6835a2
@ -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(
|
||||||
|
@ -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, ""));
|
||||||
|
@ -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;
|
||||||
|
@ -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.
|
||||||
*/
|
*/
|
||||||
|
@ -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;
|
||||||
|
@ -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.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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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)
|
||||||
));
|
));
|
||||||
|
|
||||||
|
@ -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);
|
||||||
|
@ -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">
|
||||||
|
@ -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>
|
||||||
|
Loading…
Reference in New Issue
Block a user