diff --git a/.idea/dictionaries/t.xml b/.idea/dictionaries/t.xml index 6388a69ce2..98024442c2 100644 --- a/.idea/dictionaries/t.xml +++ b/.idea/dictionaries/t.xml @@ -168,6 +168,7 @@ stefanek stringset subsport + suunto szymon taavi tablename diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/GarminWorkoutParser.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/GarminWorkoutParser.java index 28217441bf..9d1a29c59d 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/GarminWorkoutParser.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/GarminWorkoutParser.java @@ -70,6 +70,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages. import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitSession; 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.FitTankSummary; 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; @@ -97,8 +98,10 @@ public class GarminWorkoutParser implements ActivitySummaryParser { private FitPhysiologicalMetrics physiologicalMetrics = null; @Nullable private FitDiveSettings diveSettings = null; + private final List diveLaps = new ArrayList<>(); @Nullable private FitDiveSummary diveSummary = null; + private final List diveTanks = new ArrayList<>(); private final List diveGases = new ArrayList<>(); private final List sets = new ArrayList<>(); private final List laps = new ArrayList<>(); @@ -195,9 +198,11 @@ public class GarminWorkoutParser implements ActivitySummaryParser { userMetrics = null; userProfile = null; physiologicalMetrics = null; + diveLaps.clear(); diveSettings = null; diveSummary = null; diveGases.clear(); + diveTanks.clear(); sets.clear(); laps.clear(); deviceInfos.clear(); @@ -264,21 +269,22 @@ public class GarminWorkoutParser implements ActivitySummaryParser { } } else if (record instanceof FitDiveSummary newDiveSummary) { LOG.trace("Dive summary: {}", newDiveSummary); - if (diveSummary != null) { - Integer referenceMesg = newDiveSummary.getReferenceMesg(); - if (referenceMesg != null && referenceMesg != 18) { - // prioritize diving session (18) over records like diving laps (19) - // wish list for apnea diving: parse the multiple laps (19) - return false; - } - + Integer referenceMesg = newDiveSummary.getReferenceMesg(); + if (referenceMesg != null && referenceMesg == 19) { + // laps + diveLaps.add(newDiveSummary); + } else if (referenceMesg == null || referenceMesg == 18) { + // sessions Integer referenceIndex = newDiveSummary.getReferenceIndex(); - if (referenceIndex != null && referenceIndex != 0) { - // prioritize first session/lap over other session/lap + if (referenceIndex == null || referenceIndex == 0) { + diveSummary = newDiveSummary; + } else { + LOG.warn("Got multiple dive sessions - NOT SUPPORTED: {}", newDiveSummary); return false; } + } else { + return false; } - diveSummary = newDiveSummary; } else if (record instanceof FitDiveGas fitDiveGas) { LOG.trace("Dive gas: {}", fitDiveGas); diveGases.add(fitDiveGas); @@ -329,6 +335,15 @@ public class GarminWorkoutParser implements ActivitySummaryParser { } else { workout = fitWorkout; } + } else if (record instanceof FitTankSummary fitTankSummary){ + final Float start = fitTankSummary.getStartPressure(); + final Float end = fitTankSummary.getEndPressure(); + final Double used = fitTankSummary.getVolumeUsed(); + if ((start != null && start > 0) + || (end != null && end > 0) + || (used != null && used != 0)) { + diveTanks.add(fitTankSummary); + } } else { return false; } @@ -411,7 +426,9 @@ 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); + if (!summaryData.add(HR_AVG, session.getAverageHeartRate(), UNIT_BPM) && physiologicalMetrics != null) { + summaryData.add(HR_AVG, physiologicalMetrics.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); @@ -449,8 +466,12 @@ public class GarminWorkoutParser implements ActivitySummaryParser { } } if (!ActivityKind.isDiving(activityKind)) { - summaryData.add(TOTAL_ASCENT, session.getTotalAscent(), UNIT_METERS); - summaryData.add(TOTAL_DESCENT, session.getTotalDescent(), UNIT_METERS); + if(!summaryData.add(TOTAL_ASCENT, session.getTotalAscent(), UNIT_METERS) && physiologicalMetrics != null){ + summaryData.add(TOTAL_ASCENT, physiologicalMetrics.getTotalAscent(), UNIT_METERS); + } + if(!summaryData.add(TOTAL_DESCENT, session.getTotalDescent(), UNIT_METERS) && physiologicalMetrics != null){ + summaryData.add(TOTAL_DESCENT, physiologicalMetrics.getTotalDescent(), UNIT_METERS); + } if (session.getAvgVam() != null) { summaryData.add(AVERAGE_ASCENT_VELOCITY, session.getAvgVam() * 3600, UNIT_METERS_PER_HOUR); } @@ -463,7 +484,7 @@ public class GarminWorkoutParser implements ActivitySummaryParser { } if (speedAvg != null) { if (ActivityKind.isPaceActivity(activityKind)) { - summaryData.add(PACE_AVG_SECONDS_KM, Math.round((60 / (speedAvg.doubleValue() * 3.6)) * 60), UNIT_SECONDS_PER_KM); + summaryData.add(PACE_AVG_SECONDS_KM, 1000.0 / speedAvg.doubleValue(), UNIT_SECONDS_PER_KM); } else if (ActivityKind.isDiving(activityKind)) { // Hide speed for diving activities } else { @@ -477,7 +498,7 @@ public class GarminWorkoutParser implements ActivitySummaryParser { } if (speedMax != null) { if (ActivityKind.isPaceActivity(activityKind)) { - summaryData.add(PACE_MAX, Math.round((60 / (speedMax.doubleValue() * 3.6)) * 60), UNIT_SECONDS_PER_KM); + summaryData.add(PACE_MAX, 1000.0 / speedMax.doubleValue(), UNIT_SECONDS_PER_KM); } else if (ActivityKind.isDiving(activityKind)) { // Hide speed for diving activities } else { @@ -489,7 +510,9 @@ public class GarminWorkoutParser implements ActivitySummaryParser { 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); + if(!summaryData.add(AVG_POWER, session.getAvgPower(), UNIT_WATT) && physiologicalMetrics != null){ + summaryData.add(AVG_POWER, physiologicalMetrics.getAveragePower(), 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); @@ -711,19 +734,48 @@ public class GarminWorkoutParser implements ActivitySummaryParser { summaryData.add(BOTTOM_TIME, diveSummary.getBottomTime(), UNIT_SECONDS); summaryData.add(AVG_DEPTH, diveSummary.getAvgDepth(), UNIT_METERS); summaryData.add(MAX_DEPTH, diveSummary.getMaxDepth(), UNIT_METERS); - if (diveSummary.getStartCns() != null) { - summaryData.add(START_CNS, diveSummary.getStartCns(), UNIT_PERCENTAGE, true); - } - summaryData.add(END_CNS, diveSummary.getEndCns(), UNIT_PERCENTAGE); - if (diveSummary.getStartN2() != null) { - summaryData.add(START_N2, diveSummary.getStartN2(), UNIT_PERCENTAGE, true); - } - summaryData.add(END_N2, diveSummary.getEndN2(), UNIT_PERCENTAGE); - summaryData.add(OXYGEN_TOXICITY, diveSummary.getO2Toxicity(), UNIT_OXYGEN_TOXICITY_UNITs); + + // force display even if value is 0 + summaryData.add(START_CNS, diveSummary.getStartCns(), UNIT_PERCENTAGE, true); + summaryData.add(END_CNS, diveSummary.getEndCns(), UNIT_PERCENTAGE, true); + summaryData.add(START_N2, diveSummary.getStartN2(), UNIT_PERCENTAGE, true); + summaryData.add(END_N2, diveSummary.getEndN2(), UNIT_PERCENTAGE, true); + summaryData.add(OXYGEN_TOXICITY, diveSummary.getO2Toxicity(), UNIT_OXYGEN_TOXICITY_UNITs, true); + summaryData.add(SURFACE_INTERVAL, diveSummary.getSurfaceInterval(), UNIT_SECONDS); summaryData.add(PRESSURE_SAC_AVG, diveSummary.getAvgPressureSac(), UNIT_BAR_PER_MINUTE); } + // unlike other diving, apnea encodes most information in the lap and not in the summary + // -> display interval table even if it is the only lap + if (diveLaps.size() > 1 || (!diveLaps.isEmpty() && null != diveLaps.get(0).getHangTime())) { + final List header = new ArrayList<>(5); + header.add("#"); + header.add(SURFACE_INTERVAL); + header.add(BOTTOM_TIME); + header.add("diving_depth"); + header.add(DIVING_HANG_TIME); + final ActivitySummaryTableBuilder tableBuilder = new ActivitySummaryTableBuilder(GROUP_INTERVALS, "intervals_header", header); + + for (int i = 0; i < diveLaps.size(); i++) { + FitDiveSummary diveLap = diveLaps.get(i); + tableBuilder.addRow( + "interval_" + i, + Arrays.asList( + new ActivitySummaryValue(i + 1, UNIT_NONE), + new ActivitySummaryValue(diveLap.getSurfaceInterval(), UNIT_SECONDS), + new ActivitySummaryValue(diveLap.getBottomTime(), UNIT_SECONDS), + new ActivitySummaryValue(diveLap.getMaxDepth(), UNIT_METERS), + new ActivitySummaryValue(diveLap.getHangTime(), UNIT_SECONDS) + ) + ); + } + + if (tableBuilder.hasRows()) { + tableBuilder.addToSummaryData(summaryData); + } + } + if (diveSettings != null) { if (diveSettings.getWaterDensity() != null) { summaryData.add(WATER_TYPE, diveSettings.getWaterDensity(), UNIT_KG_PER_M3); @@ -750,8 +802,16 @@ public class GarminWorkoutParser implements ActivitySummaryParser { 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); + if (!summaryData.add(BODY_ENERGY_AT_START, session.getBeginningBodyBattery(), UNIT_PERCENTAGE)) { + if (userMetrics != null) { + summaryData.add(BODY_ENERGY_AT_START, userMetrics.getBeginningBodyBattery(), UNIT_PERCENTAGE); + } + } + if (!summaryData.add(BODY_ENERGY_AT_END, session.getEndingBodyBattery(), UNIT_PERCENTAGE)) { + if (physiologicalMetrics != null) { + summaryData.add(BODY_ENERGY_AT_END, physiologicalMetrics.getEndingBodyBattery(), UNIT_PERCENTAGE); + } + } summaryData.add(SOLAR_INTENSITY, session.getSolarIntensity(), UNIT_PERCENTAGE); summaryData.add(BATTERY_GAIN, session.getBatteryGain(), UNIT_SECONDS); @@ -792,6 +852,49 @@ public class GarminWorkoutParser implements ActivitySummaryParser { } } + if (!diveTanks.isEmpty()) { + final ActivitySummaryTableBuilder tableBuilder = new ActivitySummaryTableBuilder(GROUP_GAS, "tanks_header", Arrays.asList( + "diving_tank", + "activity_detail_start_label", + "activity_detail_end_label", + "diving_tank_used" + )); + for(int i = 0; i< diveTanks.size(); i++) { + FitTankSummary tankSummary = diveTanks.get(i); + String label = AntGadget.Companion.formatAntID(tankSummary.getSensor()); + Float start = tankSummary.getStartPressure(); + Float end = tankSummary.getEndPressure(); + Double used = tankSummary.getVolumeUsed(); + String usedUOM = UNIT_LITER; + + if(label == null){ + label = Integer.toString(i); + } + if(start == null){ + start = 0.0f; + } + if(end == null){ + end = 0.0f; + } + if(used == null){ + used = (double)(start - end); + usedUOM = UNIT_BAR; + } + tableBuilder.addRow( + "tank_" + i, + Arrays.asList( + new ActivitySummaryValue(label, UNIT_NONE), + new ActivitySummaryValue(start, UNIT_BAR), + new ActivitySummaryValue(end, UNIT_BAR), + new ActivitySummaryValue(used, usedUOM) + ) + ); + } + if (tableBuilder.hasRows()) { + tableBuilder.addToSummaryData(summaryData); + } + } + if (userProfile != null) { summaryData.add(HR_USER_RESTING, userProfile.getRestingHeartRate(), UNIT_BPM); } @@ -907,7 +1010,7 @@ public class GarminWorkoutParser implements ActivitySummaryParser { final boolean anySwimmingLaps = laps.stream() .anyMatch(lap -> lap.getSwimStyle() != null); - if (anyValidLaps) { + if (anyValidLaps && diveLaps.isEmpty()) { // Unfortunately our tables do not yet scroll horizontally, so can't always add all possible columns final List header = new ArrayList<>(); header.add("#"); @@ -931,14 +1034,17 @@ public class GarminWorkoutParser implements ActivitySummaryParser { final List row = new ArrayList<>(); - final Double speedValue; + Number speedValue = lap.getEnhancedAvgSpeed(); final String speedUnit; - if (lap.getEnhancedAvgSpeed() != null) { + if(speedValue == null){ + speedValue = lap.getAvgSpeed(); + } + if (speedValue != null) { if (ActivityKind.isPaceActivity(activityKind)) { - speedValue = (double) Math.round((60 / (lap.getEnhancedAvgSpeed() * 3.6)) * 60); + speedValue = 1000.0 / speedValue.doubleValue(); speedUnit = UNIT_SECONDS_PER_KM; } else { - speedValue = Math.round((lap.getEnhancedAvgSpeed() * 3600 / 1000) * 100.0) / 100.0; + speedValue = speedValue.doubleValue() * 3600.0 / 1000.0; speedUnit = UNIT_KMPH; } } else { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivitySummaryData.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivitySummaryData.java index c922d6b005..556f92fb85 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivitySummaryData.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivitySummaryData.java @@ -1,4 +1,4 @@ -/* Copyright (C) 2024 José Rebelo +/* Copyright (C) 2024-2026 José Rebelo, Thomas Kuehne This file is part of Gadgetbridge. @@ -62,42 +62,51 @@ public class ActivitySummaryData { this.entries = entries; } - public void add(final String key, final Number value, final String unit) { - add(null, key, value, unit, false); + /// @return {@code true} if the value was actually added + public boolean add(final String key, final Number value, final String unit) { + return add(null, key, value, unit, false); } - public void add(final String key, final String unit, final Number value, final Number valueFallback) { + /// @return {@code true} if the value was actually added + public boolean add(final String key, final String unit, final Number value, final Number valueFallback) { if (value != null) { - add(null, key, value, unit, false); + return add(null, key, value, unit, false); } else { - add(null, key, valueFallback, unit, false); + return 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); + /// @return {@code true} if the value was actually added + public boolean add(final String key, final Number value, final String unit, boolean forceDisplay) { + return add(null, key, value, unit, forceDisplay); } - public void add(final String group, final String key, final Number value, final String unit) { - add(group, key, value, unit, false); + /// @return {@code true} if the value was actually added + public boolean add(final String group, final String key, final Number value, final String unit) { + return add(group, key, value, unit, false); } - public void add(final String group, final String key, final Number value, final String unit, boolean forceDisplay) { + /// @return {@code true} if the value was actually added + public boolean add(final String group, final String key, final Number value, final String unit, boolean forceDisplay) { if (value != null && (value.doubleValue() != 0 || forceDisplay)) { entries.put(key, new ActivitySummarySimpleEntry(group, value, unit)); + return true; } + return false; } - public void add(final String key, final String value) { - add(null, key, value); + /// @return {@code true} if the value was actually added + public boolean add(final String key, final String value) { + return add(null, key, value); } - public void add(final String group, final String key, final String value) { + public boolean add(final String group, final String key, final String value) { if (StringUtils.isBlank(key) || StringUtils.isBlank(value)) { - return; + return false; } entries.put(key, new ActivitySummarySimpleEntry(group, value, ActivitySummaryEntries.UNIT_STRING)); + return true; } public void add(final String key, final ActivitySummaryEntry entry) { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivitySummaryEntries.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivitySummaryEntries.java index 408c75cea2..b93c931fbb 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivitySummaryEntries.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivitySummaryEntries.java @@ -265,6 +265,8 @@ public class ActivitySummaryEntries { 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 UNIT_BAR = "unit_bar"; + public static final String UNIT_LITER = "unit_liter"; public static final String GROUP_PACE = "Pace"; public static final String GROUP_ACTIVITY = "Activity"; @@ -314,6 +316,7 @@ public class ActivitySummaryEntries { public static final String MAX_DEPTH = "diving_maximum_diving_depth"; public static final String WATER_TYPE = "diving_water_type"; public static final String GROUP_GAS = "diving_gas"; + public static final String DIVING_HANG_TIME = "diving_hang_time"; /** * Used to signal that this activity has a gps track. This is currently used by ActivitySummaryDetail diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3d81620d8d..ed4e083f8c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1063,7 +1063,7 @@ Nitrogen load Oxygen toxicity OTU - Surface interval + Surface Interval Clock Heart rate Resting @@ -2735,6 +2735,12 @@ bar/min kg/m³ V + bar + L + Tank + + Used + Hang Time Custom EN13319 Fresh @@ -2820,7 +2826,7 @@ Start N2 load End N2 load Dive number - Bottom time + Bottom Time m cm