Zepp OS: Parse swimming details and temperature

This commit is contained in:
José Rebelo
2026-02-13 22:16:47 +00:00
parent f121aea488
commit 37e1fc25de
6 changed files with 410 additions and 117 deletions
@@ -133,8 +133,13 @@ public class ZeppOsActivitySummaryParser extends HuamiActivitySummaryParser {
}
if (summaryProto.hasPace()) {
summaryData.add(PACE_MAX, summaryProto.getPace().getBest(), UNIT_SECONDS_PER_M);
summaryData.add(PACE_AVG_SECONDS_KM, summaryProto.getPace().getAvg() * 1000, UNIT_SECONDS_PER_KM);
if (ActivityKind.isSwimActivity(activityKind)) {
summaryData.add(PACE_MAX, summaryProto.getPace().getBest() * 100, UNIT_SECONDS_PER_100_METERS);
summaryData.add(PACE_AVG_SECONDS_KM, summaryProto.getPace().getAvg() * 100, UNIT_SECONDS_PER_100_METERS);
} else {
summaryData.add(PACE_MAX, summaryProto.getPace().getBest(), UNIT_SECONDS_PER_M);
summaryData.add(PACE_AVG_SECONDS_KM, summaryProto.getPace().getAvg() * 1000, UNIT_SECONDS_PER_KM);
}
}
if (summaryProto.hasFrequency()) {
@@ -204,6 +209,12 @@ public class ZeppOsActivitySummaryParser extends HuamiActivitySummaryParser {
summaryData.add(DESCENT_SECONDS, summaryProto.getElevation().getDownhillTime(), UNIT_SECONDS);
}
if (summaryProto.hasTemperature()) {
summaryData.add(TEMPERATURE_MIN, summaryProto.getTemperature().getMin(), UNIT_CELSIUS);
summaryData.add(TEMPERATURE_MAX, summaryProto.getTemperature().getMax(), UNIT_CELSIUS);
summaryData.add(TEMPERATURE_AVG, summaryProto.getTemperature().getAvg(), UNIT_CELSIUS);
}
if (summaryProto.hasSwimmingData()) {
summaryData.add(LAPS, summaryProto.getSwimmingData().getLaps(), UNIT_LAPS);
switch (summaryProto.getSwimmingData().getLaneLengthUnit()) {
@@ -214,15 +225,7 @@ public class ZeppOsActivitySummaryParser extends HuamiActivitySummaryParser {
summaryData.add(LANE_LENGTH, summaryProto.getSwimmingData().getLaneLength(), UNIT_YARD);
break;
}
switch (summaryProto.getSwimmingData().getStyle()) {
// TODO i18n these
case 1:
summaryData.add(SWIM_STYLE, "breaststroke");
break;
case 2:
summaryData.add(SWIM_STYLE, "freestyle");
break;
}
summaryData.add(SWIM_STYLE, getSwimStyle(summaryProto.getSwimmingData().getStyle()));
summaryData.add(STROKES, summaryProto.getSwimmingData().getStrokes(), UNIT_STROKES);
summaryData.add(STROKE_RATE_AVG, summaryProto.getSwimmingData().getAvgStrokeRate(), UNIT_STROKES_PER_MINUTE);
summaryData.add(STROKE_RATE_MAX, summaryProto.getSwimmingData().getMaxStrokeRate(), UNIT_STROKES_PER_MINUTE);
@@ -230,7 +233,7 @@ public class ZeppOsActivitySummaryParser extends HuamiActivitySummaryParser {
summaryData.add(SWOLF_INDEX, summaryProto.getSwimmingData().getSwolf(), UNIT_NONE);
}
if(summaryProto.hasMovementEvaluation()) {
if (summaryProto.hasMovementEvaluation()) {
summaryData.add(MOVEMENT_CONSISTENCY, summaryProto.getMovementEvaluation().getConsistency(), UNIT_NONE);
summaryData.add(MOVEMENT_STABILITY, summaryProto.getMovementEvaluation().getStability(), UNIT_NONE);
summaryData.add(MOVEMENT_CONTINUITY, summaryProto.getMovementEvaluation().getContinuity(), UNIT_NONE);
@@ -293,5 +296,41 @@ public class ZeppOsActivitySummaryParser extends HuamiActivitySummaryParser {
tableBuilder.addToSummaryData(summaryData);
}
final List<ZeppOsActivityTrack.SwimmingInterval> swimmingIntervals = zeppOsActivityTrack.getSwimmingIntervals();
if (!swimmingIntervals.isEmpty()) {
final ActivitySummaryTableBuilder tableBuilder = new ActivitySummaryTableBuilder(GROUP_INTERVALS, "intervals_header", Arrays.asList(
"#",
"swimming_stroke",
"heart_rate",
SWOLF_INDEX,
"pref_header_time"
));
for (final ZeppOsActivityTrack.SwimmingInterval interval : swimmingIntervals) {
tableBuilder.addRow(
"interval_" + interval.number(),
Arrays.asList(
new ActivitySummaryValue(interval.number(), UNIT_NONE),
new ActivitySummaryValue(getSwimStyle(interval.style()), UNIT_NONE),
new ActivitySummaryValue(interval.swolf(), UNIT_NONE),
new ActivitySummaryValue(interval.hr(), UNIT_NONE),
new ActivitySummaryValue(interval.durationMillis() / 1000, UNIT_SECONDS)
)
);
}
tableBuilder.addToSummaryData(summaryData);
}
}
private String getSwimStyle(final int styleCode) {
return switch (styleCode) {
// TODO i18n these
case 1 -> "breaststroke";
case 2 -> "freestyle";
case 3 -> "backstroke";
default -> "unknown: " + styleCode;
};
}
}
@@ -83,6 +83,7 @@ public class GPXExporter implements ActivityTrackExporter {
XmlSerializer ser = Xml.newSerializer();
try (FileOutputStream outputStream = new FileOutputStream(targetFile)) {
ser.setOutput(outputStream, encoding);
//ser.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
ser.startDocument(encoding, Boolean.TRUE);
ser.setPrefix("xsi", NS_XSI_URI);
ser.setPrefix(NS_TRACKPOINT_EXTENSION, NS_TRACKPOINT_EXTENSION_URI);
@@ -17,6 +17,7 @@
package nodomain.freeyourgadget.gadgetbridge.model;
import java.util.Date;
import java.util.Objects;
import androidx.annotation.Nullable;
@@ -40,6 +41,7 @@ public class ActivityPoint {
private GPSCoordinate location;
private int heartRate;
private float speed = -1;
private int strideCm = -1;
private int cadence = -1;
private int power = -1;
private float respiratoryRate = -1;
@@ -97,6 +99,14 @@ public class ActivityPoint {
this.speed = speed;
}
public int getStrideCm() {
return strideCm;
}
public void setStrideCm(int strideCm) {
this.strideCm = strideCm;
}
public int getCadence() {
return cadence;
}
@@ -126,4 +136,154 @@ public class ActivityPoint {
public double getDepth() {return depth; }
public void setDepth(final double depth) { this.depth = depth; }
@Override
public boolean equals(Object o) {
if (!(o instanceof ActivityPoint that)) return false;
return heartRate == that.heartRate &&
Float.compare(speed, that.speed) == 0 &&
strideCm == that.strideCm &&
cadence == that.cadence &&
power == that.power &&
Float.compare(respiratoryRate, that.respiratoryRate) == 0 &&
Double.compare(depth, that.depth) == 0 &&
Double.compare(temperature, that.temperature) == 0 &&
Objects.equals(time, that.time) &&
Objects.equals(location, that.location) &&
Objects.equals(description, that.description);
}
@Override
public int hashCode() {
return Objects.hash(time, location, heartRate, speed, strideCm, cadence, power, respiratoryRate, depth, temperature, description);
}
public static class Builder {
private long timeMillis;
private double latitude;
private double longitude;
private double altitude = GPSCoordinate.UNKNOWN_ALTITUDE;
private int heartRate;
private float speed = -1;
private int strideCm = -1;
private int cadence = -1;
private int power = -1;
private float respiratoryRate = -1;
private double depth = -1;
private double temperature = -273;
public long getTime() {
return timeMillis;
}
public void setTime(final long timeMillis) {
this.timeMillis = timeMillis;
}
public double getLatitude() {
return latitude;
}
public void setLatitude(final double latitude) {
this.latitude = latitude;
}
public double getLongitude() {
return longitude;
}
public void setLongitude(final double longitude) {
this.longitude = longitude;
}
public double getAltitude() {
return altitude;
}
public void setAltitude(final double altitude) {
this.altitude = altitude;
}
public int getHeartRate() {
return heartRate;
}
public void setHeartRate(final int heartRate) {
this.heartRate = heartRate;
}
public float getSpeed() {
return speed;
}
public void setSpeed(final float speed) {
this.speed = speed;
}
public int getStrideCm() {
return strideCm;
}
public void setStrideCm(int strideCm) {
this.strideCm = strideCm;
}
public int getCadence() {
return cadence;
}
public void setCadence(final int cadence) {
this.cadence = cadence;
}
public int getPower() {
return power;
}
public void setPower(final int power) {
this.power = power;
}
public float getRespiratoryRate() {
return respiratoryRate;
}
public void setRespiratoryRate(final float respiratoryRate) {
this.respiratoryRate = respiratoryRate;
}
public double getDepth() {
return depth;
}
public void setDepth(final double depth) {
this.depth = depth;
}
public double getTemperature() {
return temperature;
}
public void setTemperature(final double temperature) {
this.temperature = temperature;
}
public ActivityPoint build() {
final ActivityPoint activityPoint = new ActivityPoint();
activityPoint.setTime(new Date(timeMillis));
if (latitude != 0 && longitude != 0) {
activityPoint.setLocation(new GPSCoordinate(longitude, latitude, altitude));
}
activityPoint.setHeartRate(heartRate);
activityPoint.setSpeed(speed);
activityPoint.setCadence(cadence);
activityPoint.setStrideCm(strideCm);
activityPoint.setPower(power);
activityPoint.setRespiratoryRate(respiratoryRate);
activityPoint.setDepth(depth);
activityPoint.setTemperature(temperature);
return activityPoint;
}
}
}
@@ -16,8 +16,6 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos;
import androidx.annotation.Nullable;
import org.apache.commons.lang3.ArrayUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -27,6 +25,7 @@ import java.nio.ByteOrder;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
@@ -35,8 +34,8 @@ import java.util.TimeZone;
import nodomain.freeyourgadget.gadgetbridge.GBException;
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityPoint;
import nodomain.freeyourgadget.gadgetbridge.model.GPSCoordinate;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.AbstractHuamiActivityDetailsParser;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
public class ZeppOsActivityDetailsParser extends AbstractHuamiActivityDetailsParser {
private static final Logger LOG = LoggerFactory.getLogger(ZeppOsActivityDetailsParser.class);
@@ -47,23 +46,19 @@ public class ZeppOsActivityDetailsParser extends AbstractHuamiActivityDetailsPar
SDF.setTimeZone(TimeZone.getTimeZone("UTC"));
}
private Date timestamp;
private long offset = 0;
private long longitude;
private long latitude;
private double altitude;
private final ZeppOsActivityTrack activityTrack;
private ActivityPoint lastActivityPoint;
// We need to keep track of these separately because of the offsets
private long timestamp;
private int offset;
private int longitude;
private int latitude;
private int lastSwimIntervalIndex = 0;
private final ActivityPoint.Builder activityPointBuilder = new ActivityPoint.Builder();
public ZeppOsActivityDetailsParser(final BaseActivitySummary summary) {
this.timestamp = summary.getStartTime();
this.longitude = summary.getBaseLongitude();
this.latitude = summary.getBaseLatitude();
this.altitude = summary.getBaseAltitude();
this.activityTrack = new ZeppOsActivityTrack();
this.activityTrack.setUser(summary.getUser());
this.activityTrack.setDevice(summary.getDevice());
@@ -131,16 +126,28 @@ public class ZeppOsActivityDetailsParser extends AbstractHuamiActivityDetailsPar
case HEARTRATE:
consumeHeartRate(buf);
break;
case TEMPERATURE:
consumeTemperature(buf);
break;
case STRENGTH_SET:
consumeStrengthSet(buf);
break;
case SWIMMING_INTERVAL:
consumeSwimmingInterval(buf);
break;
case LAP:
consumeLap(buf);
break;
default:
LOG.warn("No consumer for for type {}", type);
// Consume the reported length
buf.get(new byte[length]);
final byte[] unkBytes = new byte[length];
buf.get(unkBytes);
LOG.warn(
"No consumer for for type {} at {}: {}",
type,
timestamp,
GB.hexdump(unkBytes)
);
continue;
}
@@ -153,7 +160,7 @@ public class ZeppOsActivityDetailsParser extends AbstractHuamiActivityDetailsPar
if (!unknownTypeCodes.isEmpty()) {
for (final Map.Entry<Integer, Integer> e : unknownTypeCodes.entrySet()) {
LOG.warn("Unknown type code {} seen {} times", String.format("0x%X", e.getKey()), e.getValue());
LOG.warn("Unknown type code {} seen {} times", e.getKey(), e.getValue());
}
}
@@ -210,15 +217,21 @@ public class ZeppOsActivityDetailsParser extends AbstractHuamiActivityDetailsPar
}
private void consumeTimestamp(final ByteBuffer buf) {
addNewActivityPoint();
buf.getInt(); // ?
this.timestamp = new Date(buf.getLong());
this.timestamp = buf.getLong();
this.offset = 0;
trace("Consumed timestamp");
trace("Consumed timestamp: {}", timestamp);
}
private void consumeTimestampOffset(final ByteBuffer buf) {
addNewActivityPoint();
this.offset = buf.getShort();
trace("Consumed offset: {}", offset);
}
private void consumeGpsCoords(final ByteBuffer buf, final int length) {
@@ -237,8 +250,6 @@ public class ZeppOsActivityDetailsParser extends AbstractHuamiActivityDetailsPar
// TODO which one is the time offset? Not sure it is the first
addNewGpsCoordinates();
final double longitudeDeg = convertHuamiValueToDecimalDegrees(longitude);
final double latitudeDeg = convertHuamiValueToDecimalDegrees(latitude);
@@ -251,23 +262,20 @@ public class ZeppOsActivityDetailsParser extends AbstractHuamiActivityDetailsPar
final short latitudeDelta = buf.getShort();
final short two = buf.getShort(); // ? seems to always be 2
this.longitude += longitudeDelta;
this.latitude += latitudeDelta;
// Handle additional data in Balance 2 format (16 bytes total)
if (length == 16) {
// Skip additional 8 bytes: 2-byte flag + 2x 4-byte floats (likely speed/accuracy)
buf.get(new byte[8]);
}
if (lastActivityPoint == null) {
final String timestampStr = SDF.format(new Date(timestamp.getTime() + offset));
if (this.longitude == 0 && this.latitude == 0) {
final String timestampStr = SDF.format(new Date(timestamp + offset));
LOG.warn("{}: Got GPS delta before GPS coords, ignoring", timestampStr);
return;
} else {
this.longitude += longitudeDelta;
this.latitude += latitudeDelta;
}
addNewGpsCoordinates();
trace("Consumed GPS delta: {} {} {}", longitudeDelta, latitudeDelta, two);
}
@@ -290,6 +298,7 @@ public class ZeppOsActivityDetailsParser extends AbstractHuamiActivityDetailsPar
break;
case 6:
status = "stop";
addNewActivityPoint();
break;
default:
status = String.format("unknown (0x%X)", statusCode);
@@ -306,12 +315,10 @@ public class ZeppOsActivityDetailsParser extends AbstractHuamiActivityDetailsPar
final short stride = buf.getShort(); // cm
final short pace = buf.getShort(); // sec/km
final ActivityPoint ap = getCurrentActivityPoint();
if (ap != null) {
ap.setCadence(cadence);
if (pace != 0) {
ap.setSpeed(1000f / pace); // s/km -> m/s
}
activityPointBuilder.setCadence(cadence);
activityPointBuilder.setStrideCm(stride);
if (pace != 0) {
activityPointBuilder.setSpeed(1000f / pace); // s/km -> m/s
}
trace("Consumed speed: cadence={}, stride={}, pace={}", cadence, stride, pace);
@@ -341,35 +348,31 @@ public class ZeppOsActivityDetailsParser extends AbstractHuamiActivityDetailsPar
newAltitude = altitudeRaw / 100.0f;
}
final ActivityPoint ap = getCurrentActivityPoint();
if (ap != null) {
final GPSCoordinate newCoordinate = new GPSCoordinate(
ap.getLocation().getLongitude(),
ap.getLocation().getLatitude(),
newAltitude
);
activityPointBuilder.setAltitude(newAltitude);
ap.setLocation(newCoordinate);
}
// Only update the instance altitude if we have valid data
altitude = newAltitude;
trace("Consumed altitude: {}", altitude);
trace("Consumed altitude: {}", newAltitude);
}
private void consumeHeartRate(final ByteBuffer buf) {
consumeTimestampOffset(buf);
final int heartRate = buf.get() & 0xff;
final ActivityPoint ap = getCurrentActivityPoint();
if (ap != null) {
ap.setHeartRate(heartRate);
}
activityPointBuilder.setHeartRate(heartRate);
trace("Consumed HeartRate: {}", heartRate);
}
private void consumeTemperature(final ByteBuffer buf) {
consumeTimestampOffset(buf);
final float temperature = buf.getFloat();
buf.get(); // 0?
activityPointBuilder.setTemperature(temperature);
trace("Consumed temperature: {}", temperature);
}
private void consumeStrengthSet(final ByteBuffer buf) {
buf.get(new byte[15]); // ?
final int reps = buf.getShort() & 0xffff;
@@ -382,6 +385,59 @@ public class ZeppOsActivityDetailsParser extends AbstractHuamiActivityDetailsPar
trace("Consumed strength set: reps={}, weightKg={}", reps, weight);
}
private void consumeSwimmingInterval(final ByteBuffer buf) {
consumeTimestampOffset(buf);
buf.get(); // 0?
final int interval = buf.getShort(); // starting from 1
final int poolLengthMeters = buf.getShort();
buf.get(); // 0?
final int hr = buf.get() & 0xff;
buf.getShort(); // 1?
final int style = buf.getShort() & 0xffff;
final int pace = buf.getShort() & 0xffff; // s/km
final int swolf = buf.getShort() & 0xffff;
final int strokeRate = buf.getShort() & 0xffff;
final int durationMillis = buf.getInt();
buf.getInt(); // 0x00000000?
final int strokeDistance = buf.getShort() & 0xffff;
final int calories = buf.getShort() & 0xffff;
activityTrack.addSwimmingInterval(
interval,
poolLengthMeters,
hr,
style,
pace,
swolf,
strokeRate,
durationMillis,
strokeDistance,
calories
);
final List<ActivityPoint> allPoints = activityTrack.getAllPoints();
// Fill all activity points since the last interval so we can chart it
for (int i = lastSwimIntervalIndex; i < allPoints.size(); i++) {
allPoints.get(i).setSpeed(1000f / pace);
allPoints.get(i).setCadence(strokeRate);
}
lastSwimIntervalIndex = allPoints.size();
trace(
"Consumed swimming interval {}: hr={}, style={}, pace={}, swolf={}, strokeRate={}, durationMillis={}, strokeDistance={}, calories={}",
interval,
hr,
style,
pace,
swolf,
strokeRate,
durationMillis,
strokeDistance,
calories
);
}
private void consumeLap(final ByteBuffer buf) {
buf.get(new byte[2]); // ?
final int number = buf.getShort() & 0xffff;
@@ -399,59 +455,43 @@ public class ZeppOsActivityDetailsParser extends AbstractHuamiActivityDetailsPar
trace("Consumed lap: number={}, hr={}, pace={}, calories={}, distance={}, duration={}", number, hr, pace, calories, distance, duration);
}
@Nullable
private ActivityPoint getCurrentActivityPoint() {
if (lastActivityPoint == null) {
return null;
}
// Round to the nearest second
final long currentTime = timestamp.getTime() + offset;
if (currentTime - lastActivityPoint.getTime().getTime() > 500) {
addNewGpsCoordinates();
return lastActivityPoint;
}
return lastActivityPoint;
}
private void addNewGpsCoordinates() {
final GPSCoordinate coordinate = new GPSCoordinate(
convertHuamiValueToDecimalDegrees(longitude),
convertHuamiValueToDecimalDegrees(latitude),
altitude
);
if (lastActivityPoint != null && lastActivityPoint.getLocation() != null && lastActivityPoint.getLocation().equals(coordinate)) {
// Ignore repeated location
private void addNewActivityPoint() {
if (timestamp == 0) {
return;
}
final ActivityPoint ap = new ActivityPoint(new Date(timestamp.getTime() + offset));
ap.setLocation(coordinate);
add(ap);
}
private void add(final ActivityPoint ap) {
if (ap == lastActivityPoint) {
LOG.debug("skipping point!");
return;
activityPointBuilder.setTime(Math.round((timestamp + offset) / 1000d) * 1000L);
if (longitude != 0) {
activityPointBuilder.setLongitude(convertHuamiValueToDecimalDegrees(longitude));
}
if (latitude != 0) {
activityPointBuilder.setLatitude(convertHuamiValueToDecimalDegrees(latitude));
}
final ActivityPoint ap = activityPointBuilder.build();
final List<ActivityPoint> currentSegment = activityTrack.getSegments().get(activityTrack.getSegments().size() - 1);
// We get timestamp increments by milliseconds, and data interleaved with those
// Upsert the last value for the same second if we got an update, as it will almost
// surely contain updated information
if (!currentSegment.isEmpty()) {
if (currentSegment.get(currentSegment.size() - 1).getTime().equals(ap.getTime())) {
trace("Upserting activity point at {}", ap.getTime());
currentSegment.set(currentSegment.size() - 1, ap);
} else {
trace("Adding activity point at {}", ap.getTime());
activityTrack.addTrackPoint(ap);
}
} else {
trace("Adding activity point at {}", ap.getTime());
activityTrack.addTrackPoint(ap);
}
lastActivityPoint = ap;
activityTrack.addTrackPoint(ap);
}
private void trace(final String format, final Object... args) {
final Object[] argsWithDate;
if (timestamp != null) {
argsWithDate = ArrayUtils.insert(0, args, SDF.format(new Date(timestamp.getTime() + offset)));
} else {
argsWithDate = ArrayUtils.insert(0, args, "(null)");
}
if (LOG.isTraceEnabled()) {
final Object[] argsWithDate = ArrayUtils.insert(0, args, SDF.format(new Date(activityPointBuilder.getTime())));
//noinspection StringConcatenationArgumentToLogCall
LOG.trace("{}: " + format, argsWithDate);
//noinspection StringConcatenationArgumentToLogCall
LOG.trace("{}: " + format, argsWithDate);
}
}
private enum Type {
@@ -463,18 +503,21 @@ public class ZeppOsActivityDetailsParser extends AbstractHuamiActivityDetailsPar
ALTITUDE(7, 6),
HEARTRATE(8, 3),
LAP(11, 99),
TEMPERATURE(13, 7),
STRENGTH_SET(15, 34),
SWIMMING_INTERVAL(20, 31),
//UNKNOWN_7945(7945, 6),
;
private final byte code;
private final int code;
private final int expectedLength;
Type(final int code, final int expectedLength) {
this.code = (byte) code;
this.code = code;
this.expectedLength = expectedLength;
}
public byte getCode() {
public int getCode() {
return this.code;
}
@@ -8,6 +8,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.ActivityTrack;
public class ZeppOsActivityTrack extends ActivityTrack {
public final List<StrengthSet> strengthSets = new ArrayList<>();
public final List<Lap> laps = new ArrayList<>();
public final List<SwimmingInterval> swimmingIntervals = new ArrayList<>();
public void addStrengthSet(final int reps, final float weightKg) {
strengthSets.add(new StrengthSet(reps, weightKg));
@@ -22,6 +23,32 @@ public class ZeppOsActivityTrack extends ActivityTrack {
laps.add(new Lap(number, hr, pace, calories, distance, duration));
}
public void addSwimmingInterval(
final int number,
final int poolLengthMeters,
final int hr,
final int style,
final int pace,
final int swolf,
final int strokeRate,
final int durationMillis,
final int strokeDistance,
final int calories
) {
swimmingIntervals.add(new SwimmingInterval(
number,
poolLengthMeters,
hr,
style,
pace,
swolf,
strokeRate,
durationMillis,
strokeDistance,
calories
));
}
public List<StrengthSet> getStrengthSets() {
return strengthSets;
}
@@ -30,6 +57,10 @@ public class ZeppOsActivityTrack extends ActivityTrack {
return laps;
}
public List<SwimmingInterval> getSwimmingIntervals() {
return swimmingIntervals;
}
public record StrengthSet(int reps, float weightKg) {
}
@@ -40,4 +71,16 @@ public class ZeppOsActivityTrack extends ActivityTrack {
int distance,
int duration) {
}
public record SwimmingInterval(int number,
int poolLengthMeters,
int hr,
int style,
int pace,
int swolf,
int strokeRate,
int durationMillis,
int strokeDistance,
int calories) {
}
}
+7
View File
@@ -15,6 +15,7 @@ message WorkoutSummary {
Pace pace = 10;
Altitude altitude = 13;
HeartRate heartRate = 19;
Temperature temperature = 20;
Calories calories = 16;
Frequency frequency = 18;
TrainingEffect trainingEffect = 21;
@@ -41,6 +42,12 @@ message HeartRate {
int32 min = 3; // bpm
}
message Temperature {
float avg = 1; // celsius
float max = 2; // celsius
float min = 3; // celsius
}
message Steps {
float avgCadence = 1; // steps/sec
float maxCadence = 2; // steps/sec