Huami: Improve workout charts for activities without gps

This commit is contained in:
José Rebelo
2025-12-20 19:12:14 +00:00
parent 76a844eb8a
commit bba80d3c60
3 changed files with 95 additions and 53 deletions
@@ -19,45 +19,94 @@ package nodomain.freeyourgadget.gadgetbridge.devices.huami;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.*;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.GBException;
import nodomain.freeyourgadget.gadgetbridge.activities.workouts.charts.DefaultWorkoutCharts;
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityPoint;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryData;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityTrack;
import nodomain.freeyourgadget.gadgetbridge.model.workout.Workout;
import nodomain.freeyourgadget.gadgetbridge.model.workout.WorkoutChart;
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.AbstractHuamiActivityDetailsParser;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiActivityDetailsParser;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiSportsActivityType;
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
public class HuamiActivitySummaryParser implements ActivitySummaryParser {
private static final Logger LOG = LoggerFactory.getLogger(HuamiActivitySummaryParser.class);
protected ActivitySummaryData summaryData = new ActivitySummaryData();
protected final List<WorkoutChart> charts = new ArrayList<>();
@Override
public BaseActivitySummary parseBinaryData(BaseActivitySummary summary, final boolean forDetails) {
// FIXME Do not use this
return parseWorkout(summary, forDetails).getSummary();
}
@Override
public Workout parseWorkout(final BaseActivitySummary summary, final boolean forDetails) {
Date startTime = summary.getStartTime();
if (startTime == null) {
LOG.error("Due to a bug, we can only parse the summary when startTime is already set");
return null;
}
summaryData = new ActivitySummaryData();
parseBinaryData(summary, startTime, forDetails);
charts.clear();
parseBinaryData(summary, startTime);
summary.setSummaryData(summaryData.toString());
return summary;
if (forDetails && !StringUtils.isBlank(summary.getRawDetailsPath())) {
try {
final File inputFile = FileUtils.tryFixPath(new File(summary.getRawDetailsPath()));
if (inputFile == null) {
LOG.warn("Raw file for details not found: {}", summary.getRawDetailsPath());
return new Workout(summary, ActivitySummaryData.fromJson(summaryData.toString()));
}
final byte[] detailsBytes;
try (InputStream inputStream = new FileInputStream(inputFile)) {
detailsBytes = FileUtils.readAll(inputStream, inputFile.length());
}
final AbstractHuamiActivityDetailsParser detailsParser = getDetailsParser(summary);
final ActivityTrack activityTrack = detailsParser.parse(detailsBytes);
enrichWithDetails(summary, activityTrack);
} catch (final Exception e) {
LOG.error("Failed enrich workout with details", e);
}
}
return new Workout(
summary,
ActivitySummaryData.fromJson(summaryData.toString()),
charts
);
}
public AbstractHuamiActivityDetailsParser getDetailsParser(final BaseActivitySummary summary) {
return new HuamiActivityDetailsParser(summary);
}
protected void parseBinaryData(BaseActivitySummary summary, Date startTime, final boolean forDetails) {
protected void parseBinaryData(BaseActivitySummary summary, Date startTime) {
final byte[] rawSummaryData = summary.getRawSummaryData();
if (rawSummaryData == null) {
return;
@@ -65,14 +114,14 @@ public class HuamiActivitySummaryParser implements ActivitySummaryParser {
final ByteBuffer buffer = ByteBuffer.wrap(rawSummaryData).order(ByteOrder.LITTLE_ENDIAN);
short version = buffer.getShort(); // version
LOG.debug("Got sport summary version " + version + " total bytes=" + buffer.capacity());
LOG.debug("Got sport summary version {} total bytes={}", version, buffer.capacity());
ActivityKind activityKind = ActivityKind.UNKNOWN;
int rawKind = BLETypeConversions.toUnsigned(buffer.getShort());
try {
HuamiSportsActivityType activityType = HuamiSportsActivityType.fromCode(rawKind);
activityKind = activityType.toActivityKind();
} catch (Exception ex) {
LOG.error("Error mapping activity kind: " + ex.getMessage(), ex);
LOG.error("Error mapping activity kind", ex);
summaryData.add("Raw Activity Kind", rawKind, UNIT_NONE);
}
summary.setActivityKind(activityKind.getCode());
@@ -370,23 +419,26 @@ public class HuamiActivitySummaryParser implements ActivitySummaryParser {
summaryData.add(LAP_PACE_AVERAGE, averageLapPace, "second");
summaryData.add(STROKES, strokes, "strokes");
summaryData.add(SWOLF_INDEX, swolfIndex, "swolf_index");
String swimStyleName = "unknown"; // TODO: translate here or keep as string identifier here?
switch (swimStyle) {
case 1:
swimStyleName = "breaststroke";
break;
case 2:
swimStyleName = "freestyle";
break;
case 3:
swimStyleName = "backstroke";
break;
case 4:
swimStyleName = "medley";
break;
}
String swimStyleName = switch (swimStyle) {
case 1 -> "breaststroke";
case 2 -> "freestyle";
case 3 -> "backstroke";
case 4 -> "medley";
default -> "unknown"; // TODO: translate here or keep as string identifier here?
};
summaryData.add(SWIM_STYLE, swimStyleName);
summaryData.add(LAPS, laps, "laps");
}
}
protected void enrichWithDetails(final BaseActivitySummary summary, final ActivityTrack activityTrack) throws IOException, GBException {
final List<ActivityPoint> allPoints = activityTrack.getAllPoints();
if (!allPoints.isEmpty()) {
charts.addAll(DefaultWorkoutCharts.buildDefaultCharts(
GBApplication.getContext(),
allPoints,
ActivityKind.fromCode(summary.getActivityKind())
));
}
}
}
@@ -20,17 +20,15 @@ import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.
import android.content.Context;
import androidx.core.content.ContextCompat;
import com.google.protobuf.InvalidProtocolBufferException;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
@@ -41,6 +39,7 @@ import nodomain.freeyourgadget.gadgetbridge.activities.workouts.entries.Activity
import nodomain.freeyourgadget.gadgetbridge.activities.workouts.entries.ActivitySummaryTableBuilder;
import nodomain.freeyourgadget.gadgetbridge.activities.workouts.entries.ActivitySummaryValue;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiActivitySummaryParser;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityTrack;
import nodomain.freeyourgadget.gadgetbridge.proto.HuamiProtos;
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
@@ -48,7 +47,6 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.AbstractHuamiA
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.ZeppOsActivityDetailsParser;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.ZeppOsActivityTrack;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.ZeppOsActivityType;
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
public class ZeppOsActivitySummaryParser extends HuamiActivitySummaryParser {
private static final Logger LOG = LoggerFactory.getLogger(ZeppOsActivitySummaryParser.class);
@@ -64,7 +62,7 @@ public class ZeppOsActivitySummaryParser extends HuamiActivitySummaryParser {
}
@Override
protected void parseBinaryData(final BaseActivitySummary summary, final Date startTime, final boolean forDetails) {
protected void parseBinaryData(final BaseActivitySummary summary, final Date startTime) {
final byte[] rawData = summary.getRawSummaryData();
if (rawData == null) {
return;
@@ -163,11 +161,11 @@ public class ZeppOsActivitySummaryParser extends HuamiActivitySummaryParser {
final List<String> zoneOrder = Arrays.asList(HR_ZONE_NA, HR_ZONE_WARM_UP, HR_ZONE_FAT_BURN, HR_ZONE_AEROBIC, HR_ZONE_ANAEROBIC, HR_ZONE_EXTREME);
final int[] zoneColors = new int[]{
0,
context.getResources().getColor(R.color.hr_zone_warm_up_color),
context.getResources().getColor(R.color.hr_zone_easy_color),
context.getResources().getColor(R.color.hr_zone_aerobic_color),
context.getResources().getColor(R.color.hr_zone_threshold_color),
context.getResources().getColor(R.color.hr_zone_maximum_color),
ContextCompat.getColor(context, R.color.hr_zone_warm_up_color),
ContextCompat.getColor(context, R.color.hr_zone_easy_color),
ContextCompat.getColor(context, R.color.hr_zone_aerobic_color),
ContextCompat.getColor(context, R.color.hr_zone_threshold_color),
ContextCompat.getColor(context, R.color.hr_zone_maximum_color),
};
for (int i = 0; i < zoneOrder.size(); i++) {
summaryData.add(
@@ -239,30 +237,15 @@ public class ZeppOsActivitySummaryParser extends HuamiActivitySummaryParser {
summaryData.add(MOVEMENT_RHYTHM, summaryProto.getMovementEvaluation().getRhythm(), UNIT_NONE);
summaryData.add(MOVEMENT_SPEED_DECAY, summaryProto.getMovementEvaluation().getSpeedDecay(), UNIT_NONE);
}
if (forDetails && !StringUtils.isBlank(summary.getRawDetailsPath())) {
try {
enrichWithDetails(summary);
} catch (final Exception e) {
LOG.error("Failed enrich summary", e);
}
}
}
private void enrichWithDetails(final BaseActivitySummary summary) throws IOException, GBException {
final File inputFile = FileUtils.tryFixPath(new File(summary.getRawDetailsPath()));
if (inputFile == null) {
protected void enrichWithDetails(final BaseActivitySummary summary, ActivityTrack activityTrack) throws IOException, GBException {
super.enrichWithDetails(summary, activityTrack);
if (!(activityTrack instanceof ZeppOsActivityTrack zeppOsActivityTrack)) {
LOG.error("ActivityTrack not instanceof ZeppOsActivityTrack: {}", activityTrack.getClass());
return;
}
final byte[] detailsBytes;
try (InputStream inputStream = new FileInputStream(inputFile)) {
detailsBytes = FileUtils.readAll(inputStream, inputFile.length());
}
final ZeppOsActivityDetailsParser detailsParser = new ZeppOsActivityDetailsParser(summary);
final ZeppOsActivityTrack activityTrack = detailsParser.parse(detailsBytes);
List<ZeppOsActivityTrack.StrengthSet> strengthSets = activityTrack.getStrengthSets();
List<ZeppOsActivityTrack.StrengthSet> strengthSets = zeppOsActivityTrack.getStrengthSets();
if (!strengthSets.isEmpty()) {
final ActivitySummaryTableBuilder tableBuilder = new ActivitySummaryTableBuilder(SETS, "sets_header", Arrays.asList(
"set",
@@ -287,7 +270,7 @@ public class ZeppOsActivitySummaryParser extends HuamiActivitySummaryParser {
tableBuilder.addToSummaryData(summaryData);
}
final List<ZeppOsActivityTrack.Lap> laps = activityTrack.getLaps();
final List<ZeppOsActivityTrack.Lap> laps = zeppOsActivityTrack.getLaps();
if (!laps.isEmpty()) {
final ActivitySummaryTableBuilder tableBuilder = new ActivitySummaryTableBuilder(LAPS, "laps_header", Arrays.asList(
"workout_lap",
@@ -20,6 +20,7 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.entities.User;
@@ -30,7 +31,7 @@ public class ActivityTrack {
private User user;
private String name;
private List<ActivityPoint> currentSegment = new ArrayList<>();
private List<List<ActivityPoint>> segments = new ArrayList<List<ActivityPoint>>() {{
private List<List<ActivityPoint>> segments = new ArrayList<>() {{
add(currentSegment);
}};
@@ -77,6 +78,12 @@ public class ActivityTrack {
return segments;
}
public List<ActivityPoint> getAllPoints() {
return getSegments().stream()
.flatMap(Collection::stream)
.collect(Collectors.toList());
}
public Date getBaseTime() {
return baseTime;
}