diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/GBApplication.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/GBApplication.java index 759413e51..22c2af23d 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/GBApplication.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/GBApplication.java @@ -127,7 +127,7 @@ public class GBApplication extends Application { private static SharedPreferences sharedPrefs; private static final String PREFS_VERSION = "shared_preferences_version"; //if preferences have to be migrated, increment the following and add the migration logic in migratePrefs below; see http://stackoverflow.com/questions/16397848/how-can-i-migrate-android-preferences-with-a-new-version - private static final int CURRENT_PREFS_VERSION = 40; + private static final int CURRENT_PREFS_VERSION = 41; private static final LimitedQueue mIDSenderLookup = new LimitedQueue<>(16); private static GBPrefs prefs; @@ -1823,6 +1823,14 @@ public class GBApplication extends Application { } } + if (oldVersion < 41) { + // Add vo2max widget. + final String dashboardWidgetsOrder = sharedPrefs.getString("pref_dashboard_widgets_order", null); + if (!StringUtils.isBlank(dashboardWidgetsOrder) && !dashboardWidgetsOrder.contains("vo2max")) { + editor.putString("pref_dashboard_widgets_order", dashboardWidgetsOrder + ",vo2max"); + } + } + editor.putString(PREFS_VERSION, Integer.toString(CURRENT_PREFS_VERSION)); editor.apply(); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/DashboardFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/DashboardFragment.java index b994a8a01..64aeeaf87 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/DashboardFragment.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/DashboardFragment.java @@ -75,6 +75,9 @@ import nodomain.freeyourgadget.gadgetbridge.activities.dashboard.DashboardStress import nodomain.freeyourgadget.gadgetbridge.activities.dashboard.DashboardStressSegmentedWidget; import nodomain.freeyourgadget.gadgetbridge.activities.dashboard.DashboardStressSimpleWidget; import nodomain.freeyourgadget.gadgetbridge.activities.dashboard.DashboardTodayWidget; +import nodomain.freeyourgadget.gadgetbridge.activities.dashboard.DashboardVO2MaxCyclingWidget; +import nodomain.freeyourgadget.gadgetbridge.activities.dashboard.DashboardVO2MaxAnyWidget; +import nodomain.freeyourgadget.gadgetbridge.activities.dashboard.DashboardVO2MaxRunningWidget; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind; import nodomain.freeyourgadget.gadgetbridge.util.DashboardUtils; @@ -298,6 +301,15 @@ public class DashboardFragment extends Fragment implements MenuProvider { case "hrv": widget = DashboardHrvWidget.newInstance(dashboardData); break; + case "vo2max_running": + widget = DashboardVO2MaxRunningWidget.newInstance(dashboardData); + break; + case "vo2max_cycling": + widget = DashboardVO2MaxCyclingWidget.newInstance(dashboardData); + break; + case "vo2max": + widget = DashboardVO2MaxAnyWidget.newInstance(dashboardData); + break; default: LOG.error("Unknown dashboard widget {}", widgetName); continue; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/HeartRateDailyFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/HeartRateDailyFragment.java index 70ab7206b..6e0584b56 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/HeartRateDailyFragment.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/HeartRateDailyFragment.java @@ -246,7 +246,7 @@ public class HeartRateDailyFragment extends AbstractChartFragment runningEntries = new ArrayList<>(); List cyclingEntries = new ArrayList<>(); - List generalEntries = new ArrayList<>(); vo2MaxData.records.forEach((record) -> { float nd = (float) (record.timestamp - this.tsFrom) / (60 * 60 * 24); switch (record.type) { @@ -170,54 +157,25 @@ public class VO2MaxFragment extends AbstractChartFragment lineDataSets = new ArrayList<>(); - if (supportsVO2MaxGeneral(device)) { - VO2MaxRecord latestGeneralRecord = vo2MaxData.getLatestValue(Vo2MaxSample.Type.GENERAL); - float generalVO2MaxValue = calculateVO2maxGaugeValue(vo2MaxRanges, latestGeneralRecord != null ? latestGeneralRecord.value : 0); - gaugeDrawer.drawSegmentedGauge(vo2MaxGeneralGauge, colors, segments, generalVO2MaxValue, false, true); - vo2MaxGeneralValue.setText(String.valueOf(latestGeneralRecord != null ? Math.round(latestGeneralRecord.value) : "-")); - lineDataSets.add(createDataSet(generalEntries, getResources().getColor(R.color.vo2max_general_char_line_color), getString(R.string.vo2_max_general))); - } if (supportsVO2MaxRunning(device)) { VO2MaxRecord latestRunningRecord = vo2MaxData.getLatestValue(Vo2MaxSample.Type.RUNNING); float runningVO2MaxValue = calculateVO2maxGaugeValue(vo2MaxRanges, latestRunningRecord != null ? latestRunningRecord.value : 0); vo2MaxRunningValue.setText(String.valueOf(latestRunningRecord != null ? Math.round(latestRunningRecord.value) : "-")); gaugeDrawer.drawSegmentedGauge(vo2MaxRunningGauge, colors, segments, runningVO2MaxValue, false, true); - lineDataSets.add(createDataSet(runningEntries, getResources().getColor(R.color.vo2max_running_char_line_color), getString(R.string.vo2_max_running))); + lineDataSets.add(createDataSet(runningEntries, getResources().getColor(R.color.vo2max_running_char_line_color), getString(R.string.vo2max_running))); } if (supportsVO2MaxCycling(device)) { VO2MaxRecord latestCyclingRecord = vo2MaxData.getLatestValue(Vo2MaxSample.Type.CYCLING); float cyclingVO2MaxValue = calculateVO2maxGaugeValue(vo2MaxRanges, latestCyclingRecord != null ? latestCyclingRecord.value : 0); gaugeDrawer.drawSegmentedGauge(vo2MaxCyclingGauge, colors, segments, cyclingVO2MaxValue, false, true); vo2MaxCyclingValue.setText(String.valueOf(latestCyclingRecord != null ? Math.round(latestCyclingRecord.value) : "-")); - lineDataSets.add(createDataSet(cyclingEntries, getResources().getColor(R.color.vo2max_cycling_char_line_color), getString(R.string.vo2_max_cycling))); + lineDataSets.add(createDataSet(cyclingEntries, getResources().getColor(R.color.vo2max_cycling_char_line_color), getString(R.string.vo2max_cycling))); } final LineData lineData = new LineData(lineDataSets); vo2MaxChart.getXAxis().setValueFormatter(getVO2MaxLineChartValueFormatter()); @@ -320,7 +278,10 @@ public class VO2MaxFragment extends AbstractChartFragment chart) {} + protected void setupLegend(Chart chart) { + chart.getLegend().setTextColor(LEGEND_TEXT_COLOR); + chart.getLegend().setWordWrapEnabled(true); + } protected static class VO2MaxRecord { float value; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/dashboard/AbstractDashboardVO2MaxWidget.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/dashboard/AbstractDashboardVO2MaxWidget.java new file mode 100644 index 000000000..d8b6ea748 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/dashboard/AbstractDashboardVO2MaxWidget.java @@ -0,0 +1,124 @@ +package nodomain.freeyourgadget.gadgetbridge.activities.dashboard; + +import androidx.annotation.Nullable; +import androidx.core.content.ContextCompat; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.Serializable; +import java.util.List; + +import nodomain.freeyourgadget.gadgetbridge.GBApplication; +import nodomain.freeyourgadget.gadgetbridge.R; +import nodomain.freeyourgadget.gadgetbridge.activities.DashboardFragment; +import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; +import nodomain.freeyourgadget.gadgetbridge.devices.Vo2MaxSampleProvider; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.model.Vo2MaxSample; + +public abstract class AbstractDashboardVO2MaxWidget extends AbstractGaugeWidget implements DashboardVO2MaxWidgetInterface { + protected static final Logger LOG = LoggerFactory.getLogger(AbstractDashboardVO2MaxWidget.class); + + public AbstractDashboardVO2MaxWidget(int label, @Nullable String targetActivityTab) { + super(label, targetActivityTab); + } + + @Override + protected void populateData(final DashboardFragment.DashboardData dashboardData) { + final List devices = getSupportedDevices(dashboardData); + final VO2MaxData data = new VO2MaxData(); + + // Latest vo2max sample. + Vo2MaxSample sample = null; + try (DBHandler dbHandler = GBApplication.acquireDB()) { + for (GBDevice dev : devices) { + final Vo2MaxSampleProvider sampleProvider = (Vo2MaxSampleProvider) dev.getDeviceCoordinator().getVo2MaxSampleProvider(dev, dbHandler.getDaoSession()); + final Vo2MaxSample latestSample = sampleProvider.getLatestSample(getVO2MaxType(), dashboardData.timeTo * 1000L); + if (latestSample != null && (sample == null || latestSample.getTimestamp() > sample.getTimestamp())) { + sample = latestSample; + } + } + + if (sample != null) { + data.value = sample.getValue(); + } + + } catch (final Exception e) { + LOG.error("Could not get vo2max for today", e); + } + + dashboardData.put(getWidgetKey(), data); + } + + public static int[] getColors() { + return new int[]{ + ContextCompat.getColor(GBApplication.getContext(), R.color.vo2max_value_poor_color), + ContextCompat.getColor(GBApplication.getContext(), R.color.vo2max_value_fair_color), + ContextCompat.getColor(GBApplication.getContext(), R.color.vo2max_value_good_color), + ContextCompat.getColor(GBApplication.getContext(), R.color.vo2max_value_excellent_color), + ContextCompat.getColor(GBApplication.getContext(), R.color.vo2max_value_superior_color), + }; + } + + public static float[] getSegments() { + return new float[] { + 0.20F, + 0.20F, + 0.20F, + 0.20F, + 0.20F, + }; + } + + public static float[] getVO2MaxRanges() { + return new float[] { + 55.4F, + 51.1F, + 45.4F, + 41.7F, + 0.0F, + }; + } + + @Override + protected void draw(final DashboardFragment.DashboardData dashboardData) { + final VO2MaxData vo2MaxData = (VO2MaxData) dashboardData.get(getWidgetKey()); + if (vo2MaxData == null) { + drawSimpleGauge(0, -1); + return; + } + + final int[] colors = getColors(); + final float[] segments = getSegments(); + final float[] vo2MaxRanges = getVO2MaxRanges(); + float vo2MaxValue = calculateVO2maxGaugeValue(vo2MaxRanges, vo2MaxData.value != -1 ? vo2MaxData.value : 0); + setText(String.valueOf(vo2MaxData.value != -1 ? Math.round(vo2MaxData.value) : "-")); + drawSegmentedGauge( + colors, + segments, + vo2MaxValue, + false, + true + ); + } + + private float calculateVO2maxGaugeValue(float[] vo2MaxRanges, float vo2MaxValue) { + float value = -1; + for (int i = 0; i < vo2MaxRanges.length; i++) { + if (vo2MaxValue - vo2MaxRanges[i] > 0) { + float rangeValue = i - 1 >= 0 ? vo2MaxRanges[i-1] : 60F; + float rangeDiff = rangeValue - vo2MaxRanges[i]; + float valueDiff = vo2MaxValue - vo2MaxRanges[i]; + float multiplayer = valueDiff / rangeDiff; + value = (4 - i) * 0.2F + 0.2F * (multiplayer > 1 ? 1 : multiplayer) ; + break; + } + } + return value; + } + + private static class VO2MaxData implements Serializable { + private float value = -1; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/dashboard/AbstractGaugeWidget.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/dashboard/AbstractGaugeWidget.java index 52a959c42..233659c2c 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/dashboard/AbstractGaugeWidget.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/dashboard/AbstractGaugeWidget.java @@ -16,22 +16,14 @@ along with this program. If not, see . */ package nodomain.freeyourgadget.gadgetbridge.activities.dashboard; -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.Paint; -import android.graphics.PorterDuff; -import android.graphics.PorterDuffXfermode; import android.os.AsyncTask; import android.os.Bundle; -import android.util.TypedValue; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; -import androidx.annotation.ColorInt; import androidx.annotation.Nullable; import androidx.annotation.StringRes; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/dashboard/DashboardVO2MaxAnyWidget.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/dashboard/DashboardVO2MaxAnyWidget.java new file mode 100644 index 000000000..af98d1bcb --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/dashboard/DashboardVO2MaxAnyWidget.java @@ -0,0 +1,36 @@ +package nodomain.freeyourgadget.gadgetbridge.activities.dashboard; + +import android.os.Bundle; + +import nodomain.freeyourgadget.gadgetbridge.R; +import nodomain.freeyourgadget.gadgetbridge.activities.DashboardFragment; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.model.Vo2MaxSample; + +public class DashboardVO2MaxAnyWidget extends AbstractDashboardVO2MaxWidget { + + public DashboardVO2MaxAnyWidget() { + super(R.string.vo2max, "vo2max"); + } + + public static DashboardVO2MaxAnyWidget newInstance(final DashboardFragment.DashboardData dashboardData) { + final DashboardVO2MaxAnyWidget fragment = new DashboardVO2MaxAnyWidget(); + final Bundle args = new Bundle(); + args.putSerializable(ARG_DASHBOARD_DATA, dashboardData); + fragment.setArguments(args); + return fragment; + } + + public Vo2MaxSample.Type getVO2MaxType() { + return Vo2MaxSample.Type.ANY; + } + + public String getWidgetKey() { + return "vo2max"; + } + + @Override + protected boolean isSupportedBy(final GBDevice device) { + return device.getDeviceCoordinator().supportsVO2Max(); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/dashboard/DashboardVO2MaxCyclingWidget.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/dashboard/DashboardVO2MaxCyclingWidget.java new file mode 100644 index 000000000..94d45f47d --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/dashboard/DashboardVO2MaxCyclingWidget.java @@ -0,0 +1,36 @@ +package nodomain.freeyourgadget.gadgetbridge.activities.dashboard; + +import android.os.Bundle; + +import nodomain.freeyourgadget.gadgetbridge.R; +import nodomain.freeyourgadget.gadgetbridge.activities.DashboardFragment; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.model.Vo2MaxSample; + +public class DashboardVO2MaxCyclingWidget extends AbstractDashboardVO2MaxWidget { + + public DashboardVO2MaxCyclingWidget() { + super(R.string.vo2max_cycling, "vo2max"); + } + + public static DashboardVO2MaxCyclingWidget newInstance(final DashboardFragment.DashboardData dashboardData) { + final DashboardVO2MaxCyclingWidget fragment = new DashboardVO2MaxCyclingWidget(); + final Bundle args = new Bundle(); + args.putSerializable(ARG_DASHBOARD_DATA, dashboardData); + fragment.setArguments(args); + return fragment; + } + + public Vo2MaxSample.Type getVO2MaxType() { + return Vo2MaxSample.Type.CYCLING; + } + + public String getWidgetKey() { + return "vo2max_cycling"; + } + + @Override + protected boolean isSupportedBy(final GBDevice device) { + return device.getDeviceCoordinator().supportsVO2MaxCycling(); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/dashboard/DashboardVO2MaxRunningWidget.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/dashboard/DashboardVO2MaxRunningWidget.java new file mode 100644 index 000000000..544c0cf06 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/dashboard/DashboardVO2MaxRunningWidget.java @@ -0,0 +1,36 @@ +package nodomain.freeyourgadget.gadgetbridge.activities.dashboard; + +import android.os.Bundle; + +import nodomain.freeyourgadget.gadgetbridge.R; +import nodomain.freeyourgadget.gadgetbridge.activities.DashboardFragment; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.model.Vo2MaxSample; + +public class DashboardVO2MaxRunningWidget extends AbstractDashboardVO2MaxWidget { + + public DashboardVO2MaxRunningWidget() { + super(R.string.vo2max_running, "vo2max"); + } + + public static DashboardVO2MaxRunningWidget newInstance(final DashboardFragment.DashboardData dashboardData) { + final DashboardVO2MaxRunningWidget fragment = new DashboardVO2MaxRunningWidget(); + final Bundle args = new Bundle(); + args.putSerializable(ARG_DASHBOARD_DATA, dashboardData); + fragment.setArguments(args); + return fragment; + } + + public Vo2MaxSample.Type getVO2MaxType() { + return Vo2MaxSample.Type.RUNNING; + } + + public String getWidgetKey() { + return "vo2max_running"; + } + + @Override + protected boolean isSupportedBy(final GBDevice device) { + return device.getDeviceCoordinator().supportsVO2MaxRunning(); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/dashboard/DashboardVO2MaxWidgetInterface.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/dashboard/DashboardVO2MaxWidgetInterface.java new file mode 100644 index 000000000..10ccc1c10 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/dashboard/DashboardVO2MaxWidgetInterface.java @@ -0,0 +1,8 @@ +package nodomain.freeyourgadget.gadgetbridge.activities.dashboard; + +import nodomain.freeyourgadget.gadgetbridge.model.Vo2MaxSample; + +public interface DashboardVO2MaxWidgetInterface { + Vo2MaxSample.Type getVO2MaxType(); + String getWidgetKey(); +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractDeviceCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractDeviceCoordinator.java index 8e7042abe..935877260 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractDeviceCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractDeviceCoordinator.java @@ -490,11 +490,6 @@ public abstract class AbstractDeviceCoordinator implements DeviceCoordinator { return false; } - @Override - public boolean supportsVO2MaxGeneral() { - return false; - } - @Override public boolean supportsActivityTabs() { return supportsActivityTracking(); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceCoordinator.java index 8e81ccb72..b58ab0903 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceCoordinator.java @@ -221,7 +221,6 @@ public interface DeviceCoordinator { boolean supportsHrvMeasurement(); boolean supportsVO2Max(); boolean supportsVO2MaxCycling(); - boolean supportsVO2MaxGeneral(); boolean supportsVO2MaxRunning(); boolean supportsSleepMeasurement(); boolean supportsStepCounter(); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/GarminVo2MaxSampleProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/GarminVo2MaxSampleProvider.java index b84e03836..e7133ad81 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/GarminVo2MaxSampleProvider.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/GarminVo2MaxSampleProvider.java @@ -146,7 +146,7 @@ public class GarminVo2MaxSampleProvider implements Vo2MaxSampleProvider - - - - - - - - + android:text="@string/running" /> + android:text="@string/cycling" /> diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index 4086cb21d..595edaa03 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -4177,11 +4177,13 @@ @string/active_time @string/menuitem_sleep @string/body_energy - @string/vo2max @string/menuitem_stress_simple @string/menuitem_stress_segmented @string/menuitem_stress_breakdown @string/hrv + @string/vo2max + @string/vo2max_running + @string/vo2max_cycling @@ -4192,11 +4194,13 @@ activetime sleep bodyenergy - vo2max stress_simple stress_segmented stress_breakdown hrv + vo2max + vo2max_running + vo2max_cycling @@ -4209,5 +4213,6 @@ bodyenergy stress_segmented hrv + vo2max diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index e791b7886..054a3d26d 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -53,7 +53,6 @@ #d12a2a #46acea #59b22c - #824be3 #d93832 #ffa703 #04c79c diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b2d8d010e..76b65957d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1623,9 +1623,8 @@ %1$d ms %1$d-%2$d ms Baseline - VO2Max Running - VO2Max Cycling - VO2Max General + Running + Cycling %1$d bpm %1$,.2f km Gained @@ -1993,9 +1992,6 @@ Warning! Note No data - VO2Max - Running VO2Max - Cycling VO2Max LED Color @@ -2537,6 +2533,8 @@ HRV Status Body Energy VO2 Max + VO2 Max (Running) + VO2 Max (Cycling) 30 Days Timeline Ambient Sound Control Sound Control