Garmin: enhance activity summaries

- prioritise names: workout name > session sports profile > sport name
- gracefully fallback from Enhanced Respiration to plain Respiration
- gracefully fallback from Enhanced Speed to plain Speed
- add gear info showing last battery reading of connected devices
- add altitude min/max/avg
- add user heart rate (min/max/resting)
- add workout feel (0 - 100%)
- add device battery state (initial / final)
- add ebike battery state (initial / final)
- add more recovery, work, energy statistics
- diving: add SAC (Surface Air Consumption)
- diving: add water density with fallback to water type
- diving: ignore disabled gases
This commit is contained in:
Thomas Kuehne
2026-04-14 05:00:58 +00:00
parent 1b8c103610
commit 426a88f020
14 changed files with 530 additions and 34 deletions
@@ -100,6 +100,9 @@ object ActivitySummaryGroup {
ActivitySummaryEntries.HR_AVG,
ActivitySummaryEntries.HR_MAX,
ActivitySummaryEntries.HR_MIN,
ActivitySummaryEntries.RECOVERY_HR,
ActivitySummaryEntries.HR_USER_RESTING,
ActivitySummaryEntries.HR_USER_MAX,
)
)
@@ -180,6 +183,7 @@ object ActivitySummaryGroup {
ActivitySummaryEntries.AVG_RIGHT_POWER_PHASE_PEAK,
ActivitySummaryEntries.AVG_POWER_SEATING,
ActivitySummaryEntries.AVG_POWER_STANDING,
ActivitySummaryEntries.TOTAL_WORK,
)
)
@@ -240,6 +244,10 @@ object ActivitySummaryGroup {
ActivitySummaryEntries.MAX_CADENCE_SEATING,
ActivitySummaryEntries.FRONT_GEAR_SHIFTS,
ActivitySummaryEntries.REAR_GEAR_SHIFTS,
ActivitySummaryEntries.BATTERY_LEVEL_EBIKE_START,
ActivitySummaryEntries.BATTERY_LEVEL_EBIKE_END,
ActivitySummaryEntries.MOUNTAIN_BIKE_GRIT_SCORE,
ActivitySummaryEntries.MOUNTAIN_BIKE_FLOW_SCORE,
)
)
@@ -248,13 +256,19 @@ object ActivitySummaryGroup {
ActivitySummaryEntries.GROUP_TRAINING_EFFECT, listOf<String>(
ActivitySummaryEntries.TRAINING_EFFECT_AEROBIC,
ActivitySummaryEntries.TRAINING_EFFECT_ANAEROBIC,
ActivitySummaryEntries.TRAINING_EFFECT_TOTAL,
ActivitySummaryEntries.WORKOUT_LOAD,
ActivitySummaryEntries.TRAINING_LOAD,
ActivitySummaryEntries.INTENSITY_FACTOR,
ActivitySummaryEntries.TRAINING_STRESS_SCORE,
ActivitySummaryEntries.MAXIMUM_OXYGEN_UPTAKE,
ActivitySummaryEntries.RECOVERY_TIME_REMAINING_AT_START,
ActivitySummaryEntries.RECOVERY_TIME,
ActivitySummaryEntries.BODY_ENERGY_AT_START,
ActivitySummaryEntries.BODY_ENERGY_AT_END,
ActivitySummaryEntries.LACTATE_THRESHOLD_HR,
ActivitySummaryEntries.RATING_OF_PERCEIVED_EXERTION,
ActivitySummaryEntries.WORKOUT_FEEL,
)
)
@@ -298,9 +312,14 @@ object ActivitySummaryGroup {
ActivitySummaryEntries.BOTTOM_TIME,
ActivitySummaryEntries.OXYGEN_TOXICITY,
ActivitySummaryEntries.SURFACE_INTERVAL,
ActivitySummaryEntries.PRESSURE_SAC_AVG,
ActivitySummaryEntries.WATER_TYPE
)
)
// Diving Gas
put(ActivitySummaryEntries.GROUP_GAS, listOf())
// Recovery Heart Rate
put(ActivitySummaryEntries.GROUP_RECOVERY_HEART_RATE, listOf<String>())
@@ -359,6 +378,16 @@ object ActivitySummaryGroup {
ActivitySummaryEntries.STRIDE_TOTAL,
))
// Gear Info - e.g. last battery level
put(
ActivitySummaryEntries.GROUP_GEAR_INFO, listOf(
ActivitySummaryEntries.BATTERY_LEVEL_START,
ActivitySummaryEntries.BATTERY_LEVEL_END,
ActivitySummaryEntries.BATTERY_GAIN
)
)
// Other
put(
ActivitySummaryEntries.GROUP_OTHER, listOf<String>(
@@ -367,6 +396,10 @@ object ActivitySummaryGroup {
ActivitySummaryEntries.CALORIES_BURNT,
ActivitySummaryEntries.CALORIES_TOTAL,
ActivitySummaryEntries.CALORIES_RESTING,
ActivitySummaryEntries.CALORIES_CONSUMED,
ActivitySummaryEntries.SPO2_AVG,
ActivitySummaryEntries.STRESS_AVG,
ActivitySummaryEntries.SOLAR_INTENSITY,
)
)
}
@@ -27,6 +27,10 @@ public class ActivitySummaryTableBuilder {
rows.put(rowId, row);
}
public boolean hasRows() {
return !rows.isEmpty();
}
public void addToSummaryData(final ActivitySummaryData summaryData) {
final int numColumns = headerColumns.size();
final boolean[] anyNonNullOrZero = new boolean[numColumns];
@@ -32,7 +32,9 @@ import java.util.Arrays;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.TreeMap;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.workouts.charts.DefaultWorkoutCharts;
@@ -51,9 +53,14 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.FitFile;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordData;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.enums.GarminSport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.exception.FitParseException;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionBatteryStatus.BatteryStatus;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionExerciseCategory.ExerciseCategory;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionMeasurementSystem;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionWaterType.WaterType;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitDeviceInfo;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitDeviceStatus;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitDiveGas;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitDiveSettings;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitDiveSummary;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitLap;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitPhysiologicalMetrics;
@@ -62,7 +69,9 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitSet;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitSport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitTimeInZone;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitUserMetrics;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitUserProfile;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitWorkout;
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
@@ -76,12 +85,25 @@ public class GarminWorkoutParser implements ActivitySummaryParser {
private List<ActivityPoint> sessionActivityPoints;
private FitSession session = null;
private FitSport sport = null;
private FitUserMetrics userMetrics = null;
private FitUserProfile userProfile = null;
private FitPhysiologicalMetrics physiologicalMetrics = null;
private FitDiveSettings diveSettings = null;
private FitDiveSummary diveSummary = null;
private final List<FitDiveGas> diveGases = new ArrayList<>();
private final List<FitSet> sets = new ArrayList<>();
private final List<FitLap> laps = new ArrayList<>();
private final Map<Integer, FitDeviceInfo> deviceInfos = new TreeMap<>();
@Nullable
private FitDeviceStatus deviceStatusStart = null;
@Nullable
private FitDeviceStatus deviceStatusEnd = null;
@Nullable
private Number ebikeBatteryStart = null;
@Nullable
private Number ebikeBatteryEnd = null;
@Nullable
private FitWorkout workout = null;
public GarminWorkoutParser(final Context context) {
this.context = context;
@@ -161,17 +183,33 @@ public class GarminWorkoutParser implements ActivitySummaryParser {
activityPoints.clear();
session = null;
sport = null;
userMetrics = null;
userProfile = null;
physiologicalMetrics = null;
diveSettings = null;
diveSummary = null;
diveGases.clear();
sets.clear();
laps.clear();
deviceInfos.clear();
deviceStatusStart = null;
deviceStatusEnd = null;
ebikeBatteryStart = null;
ebikeBatteryEnd = null;
workout = null;
}
public boolean handleRecord(final RecordData record) {
if (record instanceof FitRecord) {
activityPoints.add(((FitRecord) record).toActivityPoint());
if (record instanceof FitRecord fitRecord) {
activityPoints.add(fitRecord.toActivityPoint());
final Integer ebikeBattery = fitRecord.getEbikeBatteryLevel();
if (ebikeBattery != null) {
if (ebikeBatteryStart == null) {
ebikeBatteryStart = ebikeBattery;
} else {
ebikeBatteryEnd = ebikeBattery;
}
}
} else if (record instanceof FitSession) {
LOG.debug("Session: {}", record);
if (session != null) {
@@ -209,6 +247,12 @@ public class GarminWorkoutParser implements ActivitySummaryParser {
// We only support 1 user profile
userProfile = (FitUserProfile) record;
}
} else if (record instanceof FitUserMetrics metrics) {
if (userMetrics != null) {
LOG.warn("Got multiple user metrics - NOT SUPPORTED: {}", record);
} else {
userMetrics = metrics;
}
} else if (record instanceof FitDiveSummary newDiveSummary) {
LOG.trace("Dive summary: {}", record);
if (diveSummary != null) {
@@ -229,6 +273,53 @@ public class GarminWorkoutParser implements ActivitySummaryParser {
} else if (record instanceof FitDiveGas) {
LOG.trace("Dive gas: {}", record);
diveGases.add((FitDiveGas) record);
}else if(record instanceof FitDiveSettings settings){
if (diveSettings != null) {
LOG.warn("Got multiple dive settings - NOT SUPPORTED: {}", record);
} else {
diveSettings = settings;
}
} else if (record instanceof FitDeviceInfo deviceInfo) {
Integer deviceIndex = deviceInfo.getDeviceIndex();
if (deviceIndex == null) {
// Suunto quirk
deviceIndex = Integer.MAX_VALUE;
}
boolean relevant = false;
// extract the latest battery related information
final Integer level = deviceInfo.getBatteryLevel();
if (level != null && level != 0) {
relevant = true;
} else {
final Float volt = deviceInfo.getBatteryVoltage();
if (volt != null && volt != 0) {
relevant = true;
} else {
BatteryStatus batteryStatus = deviceInfo.getBatteryStatus();
if (batteryStatus != null && batteryStatus != BatteryStatus.Unknown) {
relevant = true;
}
}
}
if (relevant) {
deviceInfos.put(deviceIndex, deviceInfo);
}
} else if (record instanceof FitDeviceStatus deviceStatus) {
if (deviceStatusStart == null) {
deviceStatusStart = deviceStatus;
} else {
deviceStatusEnd = deviceStatus;
}
// FitImporter implements the main processing for these records
return false;
} else if (record instanceof FitWorkout fitWorkout) {
if (workout != null) {
LOG.warn("Got multiple workout - NOT SUPPORTED: {}", record);
} else {
workout = fitWorkout;
}
} else {
return false;
}
@@ -245,13 +336,28 @@ public class GarminWorkoutParser implements ActivitySummaryParser {
}
final ActivityKind activityKind;
if (sport != null) {
if (StringUtils.isNullOrEmpty(summary.getName())) {
summary.setName(sport.getName());
// prioritize names: FitWorkout > FitSession > FitSport
if (StringUtils.isNullOrEmpty(summary.getName())) {
String workoutName = null;
if (workout != null) {
workoutName = workout.getName();
}
activityKind = getActivityKind(sport.getSport(), sport.getSubSport());
} else {
if (StringUtils.isNullOrEmpty(workoutName)) {
workoutName = session.getSportProfileName();
}
if (StringUtils.isNullOrEmpty(workoutName) && sport != null) {
workoutName = sport.getName();
}
if (!StringUtils.isNullOrEmpty(workoutName)) {
summary.setName(workoutName);
}
}
if (sport == null || session.getSport() != null) {
activityKind = getActivityKind(session.getSport(), session.getSubSport());
} else {
activityKind = getActivityKind(sport.getSport(), sport.getSubSport());
}
final ActivityKind.CycleUnit cycleUnit = ActivityKind.getCycleUnit(activityKind);
@@ -295,14 +401,27 @@ public class GarminWorkoutParser implements ActivitySummaryParser {
}
summaryData.add(FLUID_CONSUMED, session.getFluidConsumed(), UNIT_ML);
summaryData.add(ESTIMATED_SWEAT_LOSS, session.getEstimatedSweatLoss(), UNIT_ML);
summaryData.add(HR_MIN, session.getMinHeartRate(), UNIT_BPM);
summaryData.add(HR_AVG, session.getAverageHeartRate(), UNIT_BPM);
summaryData.add(HR_MAX, session.getMaxHeartRate(), UNIT_BPM);
summaryData.add(HRV_SDRR, session.getHrvSdrr(), UNIT_MILLISECONDS);
summaryData.add(HRV_RMSSD, session.getHrvRmssd(), UNIT_MILLISECONDS);
summaryData.add(SPO2_AVG, session.getAvgSpo2(), UNIT_PERCENTAGE);
summaryData.add(RESPIRATION_MIN, session.getEnhancedMinRespirationRate(), UNIT_BREATHS_PER_MIN);
summaryData.add(RESPIRATION_MAX, session.getEnhancedMaxRespirationRate(), UNIT_BREATHS_PER_MIN);
summaryData.add(RESPIRATION_AVG, session.getEnhancedAvgRespirationRate(), UNIT_BREATHS_PER_MIN);
summaryData.add(RESPIRATION_MIN, UNIT_BREATHS_PER_MIN,
session.getEnhancedMinRespirationRate(), session.getMinRespirationRate());
summaryData.add(RESPIRATION_MAX, UNIT_BREATHS_PER_MIN,
session.getEnhancedMaxRespirationRate(), session.getMaxRespirationRate());
summaryData.add(RESPIRATION_AVG, UNIT_BREATHS_PER_MIN,
session.getEnhancedAvgRespirationRate(), session.getAvgRespirationRate());
summaryData.add(ALTITUDE_MIN, UNIT_METERS,
session.getEnhancedMinAltitude(), session.getMinAltitude());
summaryData.add(ALTITUDE_MAX, UNIT_METERS,
session.getEnhancedMaxAltitude(), session.getMaxAltitude());
summaryData.add(ALTITUDE_AVG, UNIT_METERS,
session.getEnhancedAvgAltitude(), session.getAvgAltitude());
summaryData.add(STRESS_AVG, session.getAvgStress(), UNIT_NONE);
if (session.getAvgCadence() != null) {
if (cycleUnit == ActivityKind.CycleUnit.STEPS) {
@@ -329,29 +448,42 @@ public class GarminWorkoutParser implements ActivitySummaryParser {
}
summaryData.add(SWIM_AVG_CADENCE, session.getAvgSwimCadence(), UNIT_STROKES_PER_LENGTH);
if (session.getEnhancedAvgSpeed() != null) {
Number speedAvg = session.getEnhancedAvgSpeed();
if(speedAvg == null){
speedAvg = session.getAvgSpeed();
}
if (speedAvg != null) {
if (ActivityKind.isPaceActivity(activityKind)) {
summaryData.add(PACE_AVG_SECONDS_KM, Math.round((60 / (session.getEnhancedAvgSpeed() * 3.6)) * 60), UNIT_SECONDS);
summaryData.add(PACE_AVG_SECONDS_KM, Math.round((60 / (speedAvg.doubleValue() * 3.6)) * 60), UNIT_SECONDS);
} else if (ActivityKind.isDiving(activityKind)) {
// Hide speed for diving activities
} else {
summaryData.add(SPEED_AVG, Math.round((session.getEnhancedAvgSpeed() * 3600 / 1000) * 100.0) / 100.0, UNIT_KMPH);
summaryData.add(SPEED_AVG, speedAvg.doubleValue() * 3600 / 1000, UNIT_KMPH);
}
}
if (session.getEnhancedMaxSpeed() != null) {
Number speedMax = session.getEnhancedMaxSpeed();
if(speedMax == null){
speedMax = session.getMaxSpeed();
}
if (speedMax != null) {
if (ActivityKind.isPaceActivity(activityKind)) {
summaryData.add(PACE_MAX, Math.round((60 / (session.getEnhancedMaxSpeed() * 3.6)) * 60), UNIT_SECONDS);
} else if (ActivityKind.isDiving(activityKind)) { // Hide average speed for diving activities
summaryData.add(PACE_MAX, Math.round((60 / (speedMax.doubleValue() * 3.6)) * 60), UNIT_SECONDS);
} else if (ActivityKind.isDiving(activityKind)) {
// Hide speed for diving activities
} else {
summaryData.add(SPEED_MAX, Math.round((session.getEnhancedMaxSpeed() * 3600 / 1000) * 100.0) / 100.0, UNIT_KMPH);
summaryData.add(SPEED_MAX, speedMax.doubleValue() * 3600 / 1000, UNIT_KMPH);
}
}
summaryData.add(RECOVERY_HR, session.getRecoveryHeartRate(), UNIT_BPM);
summaryData.add(RATING_OF_PERCEIVED_EXERTION, session.getWorkoutRpe(), UNIT_PERCENTAGE);
summaryData.add(WORKOUT_FEEL, session.getWorkoutFeel(), UNIT_PERCENTAGE);
summaryData.add(AVG_POWER, session.getAvgPower(), UNIT_WATT);
summaryData.add(MAX_POWER, session.getMaxPower(), UNIT_WATT);
summaryData.add(NORMALIZED_POWER, session.getNormalizedPower(), UNIT_WATT);
summaryData.add(TOTAL_WORK, session.getTotalWork(), UNIT_JOULE);
if (session.getStandTime() != null) {
summaryData.add(STANDING_TIME, session.getStandTime() / 1000, UNIT_SECONDS);
@@ -564,7 +696,6 @@ public class GarminWorkoutParser implements ActivitySummaryParser {
}
summaryData.add(LACTATE_THRESHOLD_HR, physiologicalMetrics.getLactateThresholdHeartRate(), UNIT_BPM);
}
summaryData.add(TRAINING_LOAD, safeRound(session.getTrainingLoadPeak()), UNIT_NONE);
if (diveSummary != null) {
summaryData.add(DIVE_NUMBER, diveSummary.getDiveNumber(), UNIT_NONE);
@@ -581,24 +712,43 @@ public class GarminWorkoutParser implements ActivitySummaryParser {
summaryData.add(END_N2, diveSummary.getEndN2(), UNIT_PERCENTAGE);
summaryData.add(OXYGEN_TOXICITY, diveSummary.getO2Toxicity(), UNIT_OXYGEN_TOXICITY_UNITs);
summaryData.add(SURFACE_INTERVAL, diveSummary.getSurfaceInterval(), UNIT_SECONDS);
summaryData.add(PRESSURE_SAC_AVG, diveSummary.getAvgPressureSac(), UNIT_BAR_PER_MINUTE);
}
if (diveSettings != null) {
if (diveSettings.getWaterDensity() != null) {
summaryData.add(WATER_TYPE, diveSettings.getWaterDensity(), UNIT_KG_PER_M3);
} else {
WaterType waterType = diveSettings.getWaterType();
if (waterType != null) {
summaryData.add(WATER_TYPE, waterType.toString(context));
}
}
}
summaryData.add(TEMPERATURE_MIN, session.getMinTemperature(), UNIT_CELSIUS);
summaryData.add(TEMPERATURE_MAX, session.getMaxTemperature(), UNIT_CELSIUS);
summaryData.add(TEMPERATURE_AVG, session.getAvgTemperature(), UNIT_CELSIUS);
// MTB-specific metrics
summaryData.add("activity_type_mountain_bike", MOUNTAIN_BIKE_GRIT_SCORE, session.getTotalGrit(), UNIT_NONE);
summaryData.add("activity_type_mountain_bike", MOUNTAIN_BIKE_FLOW_SCORE, session.getAvgFlow(), UNIT_NONE);
summaryData.add(BATTERY_LEVEL_EBIKE_START, ebikeBatteryStart, UNIT_PERCENTAGE);
summaryData.add(BATTERY_LEVEL_EBIKE_END, ebikeBatteryEnd, UNIT_PERCENTAGE);
summaryData.add(TRAINING_LOAD, safeRound(session.getTrainingLoadPeak()), UNIT_NONE);
// MTB-specific metrics
summaryData.add(MOUNTAIN_BIKE_GRIT_SCORE, session.getTotalGrit(), UNIT_NONE);
summaryData.add(MOUNTAIN_BIKE_FLOW_SCORE, session.getAvgFlow(), UNIT_NONE);
summaryData.add(TRAINING_LOAD, session.getTrainingLoadPeak(), UNIT_NONE);
summaryData.add(INTENSITY_FACTOR, session.getIntensityFactor(), UNIT_NONE);
summaryData.add(TRAINING_STRESS_SCORE, session.getTrainingStressScore(), UNIT_NONE);
summaryData.add(TRAINING_EFFECT_TOTAL, session.getTotalTrainingEffect(), UNIT_NONE);
summaryData.add(BODY_ENERGY_AT_START, session.getBeginningBodyBattery(), UNIT_PERCENTAGE);
summaryData.add(BODY_ENERGY_AT_END, session.getEndingBodyBattery(), UNIT_PERCENTAGE);
summaryData.add(SOLAR_INTENSITY, safeRound(session.getSolarIntensity()), UNIT_PERCENTAGE);
summaryData.add(BATTERY_GAIN, session.getBatteryGain(), UNIT_SECONDS);
if (!diveGases.isEmpty()) {
final ActivitySummaryTableBuilder tableBuilder = new ActivitySummaryTableBuilder(GAS, "gases_header", Arrays.asList(
final ActivitySummaryTableBuilder tableBuilder = new ActivitySummaryTableBuilder(GROUP_GAS, "gases_header", Arrays.asList(
"diving_gas",
"helium_content",
"oxygen_content",
@@ -607,6 +757,12 @@ public class GarminWorkoutParser implements ActivitySummaryParser {
int i = 1;
for (final FitDiveGas gas : diveGases) {
final Integer gasStatus = gas.getStatus();
if (gasStatus != null && gasStatus == 0) {
// ignore disabled gasses
continue;
}
int helium = gas.getHeliumContent() != null ? gas.getHeliumContent() : 0;
int oxygen = gas.getOxygenContent() != null ? gas.getOxygenContent() : 0;
int nitrogen = 100 - helium - oxygen;
@@ -622,7 +778,89 @@ public class GarminWorkoutParser implements ActivitySummaryParser {
i++;
}
tableBuilder.addToSummaryData(summaryData);
if (tableBuilder.hasRows()) {
tableBuilder.addToSummaryData(summaryData);
}
}
if (userProfile != null) {
summaryData.add(HR_USER_RESTING, userProfile.getRestingHeartRate(), UNIT_BPM);
}
if (userMetrics != null) {
final Integer remainingRecoveryTime = userMetrics.getRemainingRecoveryTime();
if (remainingRecoveryTime != null && remainingRecoveryTime > 1) {
summaryData.add(RECOVERY_TIME_REMAINING_AT_START, userMetrics.getRemainingRecoveryTime() * 60, UNIT_SECONDS);
}
summaryData.add(HR_USER_MAX, userMetrics.getMaxHr(), UNIT_BPM);
}
if (!deviceInfos.isEmpty()) {
final ActivitySummaryTableBuilder tableBuilder = new ActivitySummaryTableBuilder(GROUP_GEAR_INFO, "gear_info_header", Arrays.asList(
"device",
"battery_status",
"battery_level"
));
for (final Map.Entry<Integer, FitDeviceInfo> entry : deviceInfos.entrySet()) {
final Integer deviceIndex = entry.getKey();
final FitDeviceInfo deviceInfo = entry.getValue();
Object device = deviceInfo.getProductName();
if (device == null) {
device = deviceInfo.getSerialNumber();
if (device == null) {
device = deviceIndex;
}
}
BatteryStatus rawStatus = deviceInfo.getBatteryStatus();
@Nullable final String status;
if (rawStatus != null) {
status = rawStatus.toString(context);
} else {
status = null;
}
Number level = deviceInfo.getBatteryLevel();
String level_uom = UNIT_PERCENTAGE;
if (level == null) {
level = deviceInfo.getBatteryVoltage();
level_uom = UNIT_VOLT;
}
tableBuilder.addRow(
"device_info_" + deviceIndex,
Arrays.asList(
new ActivitySummaryValue(device, UNIT_NONE),
new ActivitySummaryValue(status),
new ActivitySummaryValue(level, level_uom)
)
);
}
if (tableBuilder.hasRows()) {
tableBuilder.addToSummaryData(summaryData);
}
}
if (deviceStatusStart != null) {
Number batteryLevel = deviceStatusStart.getBatteryLevel();
String batteryUom = UNIT_PERCENTAGE;
if (batteryLevel == null) {
batteryLevel = deviceStatusStart.getBatteryVoltage();
batteryUom = UNIT_VOLT;
}
summaryData.add(BATTERY_LEVEL_START, batteryLevel, batteryUom);
}
if (deviceStatusEnd != null) {
Number batteryLevel = deviceStatusEnd.getBatteryLevel();
String batteryUom = UNIT_PERCENTAGE;
if (batteryLevel == null) {
batteryLevel = deviceStatusEnd.getBatteryVoltage();
batteryUom = UNIT_VOLT;
}
summaryData.add(BATTERY_LEVEL_END, batteryLevel, batteryUom);
}
if (!sets.isEmpty()) {
@@ -66,6 +66,14 @@ public class ActivitySummaryData {
add(null, key, value, unit, false);
}
public void add(final String key, final String unit, final Number value, final Number valueFallback) {
if (value != null) {
add(null, key, value, unit, false);
} else {
add(null, key, valueFallback, unit, false);
}
}
public void add(final String key, final Number value, final String unit, boolean forceDisplay) {
add(null, key, value, unit, forceDisplay);
}
@@ -123,6 +123,8 @@ public class ActivitySummaryEntries {
public static final String HR_AVG = "averageHR";
public static final String HR_MAX = "maxHR";
public static final String HR_MIN = "minHR";
public static final String HR_USER_RESTING = "hr_user_resting";
public static final String HR_USER_MAX = "hr_user_max";
public static final String HR_ZONE_NA = "hrZoneNa";
public static final String HR_ZONE_WARM_UP = "hrZoneWarmUp";
public static final String HR_ZONE_EASY = "hrZoneEasy";
@@ -182,6 +184,7 @@ public class ActivitySummaryEntries {
public static final String CALORIES_RESTING = "restingCalories";
public static final String TRAINING_EFFECT_AEROBIC = "aerobicTrainingEffect";
public static final String TRAINING_EFFECT_ANAEROBIC = "anaerobicTrainingEffect";
public static final String TRAINING_EFFECT_TOTAL = "training_effect_total";
public static final String WORKOUT_LOAD = "currentWorkoutLoad";
public static final String MAXIMUM_OXYGEN_UPTAKE = "maximumOxygenUptake";
public static final String RECOVERY_TIME = "recoveryTime";
@@ -190,6 +193,7 @@ public class ActivitySummaryEntries {
public static final String ESTIMATED_SWEAT_LOSS = "estimatedSweatLoss";
public static final String LACTATE_THRESHOLD_HR = "lactateThresholdHeartRate";
public static final String RATING_OF_PERCEIVED_EXERTION = "ratingOfPerceivedExertion";
public static final String WORKOUT_FEEL = "workout_feel";
public static final String CYCLING_POWER_AVERAGE = "cyclingPowerAverage";
public static final String CYCLING_POWER_MIN = "cyclingPowerMin";
@@ -258,6 +262,9 @@ public class ActivitySummaryEntries {
public static final String UNIT_KNOTS = "knots";
public static final String UNIT_CELSIUS = "unit_celsius";
public static final String UNIT_OXYGEN_TOXICITY_UNITs = "unit_oxygen_toxicity_units";
public static final String UNIT_BAR_PER_MINUTE = "unit_bar_per_minute";
public static final String UNIT_VOLT = "unit_volt";
public static final String UNIT_KG_PER_M3 = "unit_kg_per_m3";
public static final String GROUP_PACE = "Pace";
public static final String GROUP_ACTIVITY = "Activity";
@@ -283,6 +290,8 @@ public class ActivitySummaryEntries {
public static final String GROUP_MOVEMENT_EVALUATION = "movement_evaluation";
public static final String GROUP_DISTANCE = "Distance";
public static final String GROUP_STEPS = "Steps";
public static final String GROUP_BIKE = "activity_type_biking";
public static final String GROUP_GEAR_INFO = "Gear";
public static final String AVG_DEPTH = "diving_avg_depth";
public static final String START_CNS = "diving_start_cns";
public static final String END_CNS = "diving_end_cns";
@@ -292,11 +301,19 @@ public class ActivitySummaryEntries {
public static final String BOTTOM_TIME = "diving_bottom_time";
public static final String OXYGEN_TOXICITY = "diving_oxygen_toxicity";
public static final String SURFACE_INTERVAL = "diving_surface_interval";
public static final String PRESSURE_SAC_AVG = "diving_pressure_sac_avg";
public static final String RECOVERY_TIME_REMAINING_AT_START = "recovery_time_remaining_at_start";
public static final String BODY_ENERGY_AT_START = "body_energy_at_start";
public static final String BODY_ENERGY_AT_END = "body_energy_at_end";
public static final String TOTAL_WORK = "total_work";
public static final String BATTERY_LEVEL_START = "battery_level_start";
public static final String BATTERY_LEVEL_END = "battery_level_end";
public static final String BATTERY_LEVEL_EBIKE_START = "battery_level_ebike_start";
public static final String BATTERY_LEVEL_EBIKE_END = "battery_level_ebike_end";
// DIVING parameters
public static final String MAX_DEPTH = "diving_maximum_diving_depth";
public static final String WATER_TYPE = "diving_water_type";
public static final String GAS = "diving_gas";
public static final String GROUP_GAS = "diving_gas";
/**
* Used to signal that this activity has a gps track. This is currently used by ActivitySummaryDetail
@@ -4,6 +4,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.baseTypes
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionAlarm;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionAlarmLabel;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionArray;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionBatteryStatus;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionBoolean;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionCoordinate;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionCoursePoint;
@@ -12,6 +13,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefi
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionFileType;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionGoalSource;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionGoalType;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionWaterType;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionWeatherReport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionHrTimeInZone;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionHrZoneHighBoundary;
@@ -56,6 +58,8 @@ public class FieldDefinitionFactory {
case LOCATION_SYMBOL -> new FieldDefinitionLocationSymbol(localNumber, size, baseType, name, scale, offset);
case COURSE_POINT -> new FieldDefinitionCoursePoint(localNumber, size, baseType, name, scale, offset);
case WEATHER_REPORT -> new FieldDefinitionWeatherReport(localNumber, size, baseType, name);
case BATTERY_STATUS -> new FieldDefinitionBatteryStatus(localNumber, size, baseType, name, scale, offset);
case WATER_TYPE -> new FieldDefinitionWaterType(localNumber, size, baseType, name, scale, offset);
};
}
@@ -84,5 +88,7 @@ public class FieldDefinitionFactory {
LOCATION_SYMBOL,
COURSE_POINT,
WEATHER_REPORT,
BATTERY_STATUS,
WATER_TYPE
}
}
@@ -671,7 +671,7 @@ public class NativeFITMessage {
new FieldDefinitionPrimitive(6, BaseType.UINT8, "hardware_version"),
new FieldDefinitionPrimitive(7, BaseType.UINT32, "cum_operating_time"), // s
new FieldDefinitionPrimitive(10, BaseType.UINT16, "battery_voltage", 256, 0), // V
new FieldDefinitionPrimitive(11, BaseType.UINT8, "battery_status"),
new FieldDefinitionPrimitive(11, BaseType.UINT8, "battery_status", FieldDefinitionFactory.FIELD.BATTERY_STATUS),
new FieldDefinitionPrimitive(18, BaseType.ENUM, "sensor_position"),
new FieldDefinitionPrimitive(19, BaseType.STRING, "descriptor"),
new FieldDefinitionPrimitive(20, BaseType.UINT8Z, "ant_transmission_type"),
@@ -1546,7 +1546,7 @@ public class NativeFITMessage {
new FieldDefinitionPrimitive(1, BaseType.ENUM, "model"),
new FieldDefinitionPrimitive(2, BaseType.UINT8, "gf_low"), // %
new FieldDefinitionPrimitive(3, BaseType.UINT8, "gf_high"), // %
new FieldDefinitionPrimitive(4, BaseType.ENUM, "water_type"), // %
new FieldDefinitionPrimitive(4, BaseType.ENUM, "water_type", FieldDefinitionFactory.FIELD.WATER_TYPE),
new FieldDefinitionPrimitive(5, BaseType.FLOAT32, "water_density"), // kg/m^3
new FieldDefinitionPrimitive(6, BaseType.UINT8, "po2_warn", 100, 0), // %
new FieldDefinitionPrimitive(7, BaseType.UINT8, "po2_critical", 100, 0), // %
@@ -29,10 +29,12 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordDef
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordHeader;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.baseTypes.BaseType;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionAlarmLabel;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionBatteryStatus;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionCoursePoint;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionExerciseCategory;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionGoalSource;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionGoalType;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionWaterType;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionWeatherReport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionHrvStatus;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionLanguage;
@@ -366,6 +368,8 @@ public class FitCodeGen {
case LOCATION_SYMBOL -> FieldDefinitionLocationSymbol.LocationSymbol.class;
case COURSE_POINT -> FieldDefinitionCoursePoint.CoursePoint.class;
case WEATHER_REPORT -> FieldDefinitionWeatherReport.Type.class;
case BATTERY_STATUS -> FieldDefinitionBatteryStatus.BatteryStatus.class;
case WATER_TYPE -> FieldDefinitionWaterType.WaterType.class;
};
}
@@ -120,6 +120,7 @@ public enum GarminSport {
SOFTBALL(50, 0, ActivityKind.SOFTBALL),
SOFTBALL_SLOW_PITCH(51, 0, ActivityKind.SOFTBALL_SLOW_PITCH),
STOPWATCH(52, 0, ActivityKind.STOP_WATCH),
DIVING(53, 0, ActivityKind.DIVING),
SINGLE_GAS_DIVING(53, 53, ActivityKind.SCUBA_DIVING),
MULTI_GAS_DIVING(53, 54, ActivityKind.SCUBA_DIVING),
GAUGE_DIVING(53, 55, ActivityKind.DIVING),
@@ -0,0 +1,78 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import java.nio.ByteBuffer;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.FieldDefinition;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.baseTypes.BaseType;
public class FieldDefinitionBatteryStatus extends FieldDefinition {
public FieldDefinitionBatteryStatus(int localNumber, int size, BaseType baseType, String name, int scale, int offset) {
super(localNumber, size, baseType, name, scale, offset);
}
@Nullable
@Override
public Object decode(ByteBuffer byteBuffer) {
final Object rawObj = baseType.decode(byteBuffer, scale, offset);
if (rawObj != null) {
final int raw = (int) rawObj;
return BatteryStatus.fromId(raw);
}
return null;
}
@Override
public void encode(ByteBuffer byteBuffer, Object o) {
if (o instanceof BatteryStatus) {
baseType.encode(byteBuffer, (((BatteryStatus) o).getId()), scale, offset);
return;
}
baseType.encode(byteBuffer, o, scale, offset);
}
public enum BatteryStatus {
New(1, R.string.battery_status_new),
Good(2, R.string.battery_status_good),
Ok(3, R.string.battery_status_ok),
Low(4, R.string.battery_status_low),
Critical(5, R.string.battery_status_critical),
Charging(6, R.string.battery_status_charging),
Unknown(7, R.string.battery_status_unknown),
;
private final int id;
@StringRes
private final int stringRes;
BatteryStatus(int i, @StringRes int text) {
id = i;
stringRes = text;
}
@Nullable
public static BatteryStatus fromId(int id) {
for (BatteryStatus batteryStatus : values()) {
if (id == batteryStatus.id) {
return batteryStatus;
}
}
return null;
}
public int getId() {
return id;
}
public String toString(@NonNull Context context) {
return context.getString(stringRes);
}
}
}
@@ -0,0 +1,75 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import java.nio.ByteBuffer;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.FieldDefinition;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.baseTypes.BaseType;
public class FieldDefinitionWaterType extends FieldDefinition {
public FieldDefinitionWaterType(int localNumber, int size, BaseType baseType, String name, int scale, int offset) {
super(localNumber, size, baseType, name, scale, offset);
}
@Nullable
@Override
public Object decode(ByteBuffer byteBuffer) {
final Object rawObj = baseType.decode(byteBuffer, scale, offset);
if (rawObj != null) {
final int raw = (int) rawObj;
return WaterType.fromId(raw);
}
return null;
}
@Override
public void encode(ByteBuffer byteBuffer, Object o) {
if (o instanceof WaterType) {
baseType.encode(byteBuffer, (((WaterType) o).getId()), scale, offset);
return;
}
baseType.encode(byteBuffer, o, scale, offset);
}
public enum WaterType {
Fresh(0, R.string.water_type_fresh),
Salt(1, R.string.water_type_salt),
En13319(2, R.string.water_type_en13319),
Custom(3, R.string.water_type_custom),
;
private final int id;
@StringRes
private final int stringRes;
WaterType(int i, @StringRes int text) {
id = i;
stringRes = text;
}
@Nullable
public static WaterType fromId(int id) {
for (WaterType waterType : values()) {
if (id == waterType.id) {
return waterType;
}
}
return null;
}
public int getId() {
return id;
}
public String toString(@NonNull Context context) {
return context.getString(stringRes);
}
}
}
@@ -22,6 +22,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.FitRecord
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordData;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordDefinition;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordHeader;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionBatteryStatus.BatteryStatus;
/**
* WARNING: This class was auto-generated, please avoid modifying it directly.
@@ -85,8 +86,8 @@ public class FitDeviceInfo extends RecordData {
}
@Nullable
public Integer getBatteryStatus() {
return getFieldByNumber(11, Integer.class);
public BatteryStatus getBatteryStatus() {
return getFieldByNumber(11, BatteryStatus.class);
}
@Nullable
@@ -192,7 +193,7 @@ public class FitDeviceInfo extends RecordData {
return this;
}
public Builder setBatteryStatus(final Integer value) {
public Builder setBatteryStatus(final BatteryStatus value) {
setFieldByNumber(11, value);
return this;
}
@@ -22,6 +22,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.FitRecord
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordData;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordDefinition;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordHeader;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionWaterType.WaterType;
/**
* WARNING: This class was auto-generated, please avoid modifying it directly.
@@ -60,8 +61,8 @@ public class FitDiveSettings extends RecordData {
}
@Nullable
public Integer getWaterType() {
return getFieldByNumber(4, Integer.class);
public WaterType getWaterType() {
return getFieldByNumber(4, WaterType.class);
}
@Nullable
@@ -242,7 +243,7 @@ public class FitDiveSettings extends RecordData {
return this;
}
public Builder setWaterType(final Integer value) {
public Builder setWaterType(final WaterType value) {
setFieldByNumber(4, value);
return this;
}
+30
View File
@@ -2702,6 +2702,36 @@
<string name="cycling">Cycling</string>
<string name="aerobicTrainingEffect">Aerobic Effect</string>
<string name="anaerobicTrainingEffect">Anaerobic Effect</string>
<string name="battery_level_ebike_end">Final Battery Level</string>
<string name="battery_level_ebike_start">Initial Battery Level</string>
<string name="battery_level_end">Final Battery Level</string>
<string name="battery_level_start">Initial Battery Level</string>
<string name="battery_status">Battery Status</string>
<string name="battery_status_charging">Charging</string>
<string name="battery_status_critical">Critical</string>
<string name="battery_status_good">Good</string>
<string name="battery_status_invalid">Invalid</string>
<string name="battery_status_low">Low</string>
<string name="battery_status_new">New</string>
<string name="battery_status_ok">Ok</string>
<string name="battery_status_unknown">Unknown</string>
<string name="body_energy_at_end">Finishing body energy</string>
<string name="body_energy_at_start">Initial body energy</string>
<string name="device">Device</string>
<string name="diving_pressure_sac_avg">Surface air consumption</string>
<string name="hr_user_max">User Max Heart Rate</string>
<string name="hr_user_resting">Resting Heart Rate</string>
<string name="recovery_time_remaining_at_start">Initial recovery time</string>
<string name="total_work">Total Work</string>
<string name="training_effect_total">Total Effect</string>
<string name="unit_bar_per_minute">bar/min</string>
<string name="unit_kg_per_m3">kg/m³</string>
<string name="unit_volt">V</string>
<string name="water_type_custom">Custom</string>
<string name="water_type_en13319">EN13319</string>
<string name="water_type_fresh">Fresh</string>
<string name="water_type_salt">Salt</string>
<string name="workout_feel">Workout Feel</string>
<string name="currentWorkoutLoad">Workout Load</string>
<string name="maximumOxygenUptake">Maximum Oxygen Uptake</string>
<string name="fluid_consumed">Fluid consumed</string>