mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge.git
synced 2026-07-31 07:44:24 +02:00
Huami: Improve workout charts for activities without gps
This commit is contained in:
+72
-20
@@ -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())
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+15
-32
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user