1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2024-06-18 11:00:09 +02:00
Gadgetbridge/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/activity/impl/WorkoutSummaryParser.java

369 lines
17 KiB
Java

/* Copyright (C) 2023 José Rebelo
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.activity.impl;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.ACTIVE_SECONDS;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.CADENCE_AVG;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.CALORIES_BURNT;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.DISTANCE_METERS;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.HR_AVG;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.HR_MAX;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.HR_MIN;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.HR_ZONE_AEROBIC;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.HR_ZONE_ANAEROBIC;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.HR_ZONE_EXTREME;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.HR_ZONE_FAT_BURN;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.HR_ZONE_WARM_UP;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.PACE_MAX;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.PACE_MIN;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.RECOVERY_TIME;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.SPEED_MAX;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.STEPS;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.TIME_END;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.TIME_START;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.TRAINING_EFFECT_AEROBIC;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.TRAINING_EFFECT_ANAEROBIC;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_BPM;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_HOURS;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_KCAL;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_KMPH;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_METERS;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_NONE;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_SECONDS;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_SECONDS_PER_M;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_SPM;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_STEPS;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_UNIX_EPOCH_SECONDS;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.WORKOUT_LOAD;
import static nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.activity.impl.XiaomiSimpleActivityParser.XIAOMI_WORKOUT_TYPE;
import android.widget.Toast;
import androidx.annotation.Nullable;
import org.apache.commons.lang3.ArrayUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.entities.User;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser;
import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.XiaomiSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.activity.XiaomiActivityFileId;
import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.activity.XiaomiActivityParser;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
public class WorkoutSummaryParser extends XiaomiActivityParser implements ActivitySummaryParser {
private static final Logger LOG = LoggerFactory.getLogger(WorkoutSummaryParser.class);
@Override
public boolean parse(final XiaomiSupport support, final XiaomiActivityFileId fileId, final byte[] bytes) {
BaseActivitySummary summary = new BaseActivitySummary();
summary.setStartTime(fileId.getTimestamp()); // due to a bug this has to be set
summary.setEndTime(fileId.getTimestamp()); // due to a bug this has to be set
summary.setActivityKind(ActivityKind.TYPE_UNKNOWN);
summary.setRawSummaryData(ArrayUtils.addAll(fileId.toBytes(), bytes));
try {
summary = parseBinaryData(summary);
} catch (final Exception e) {
LOG.error("Failed to parse workout summary", e);
GB.toast(support.getContext(), "Failed to parse workout summary", Toast.LENGTH_LONG, GB.ERROR, e);
return false;
}
// parseBinaryData may return null in case the version is not supported
if (summary == null) {
LOG.warn("summary is null - should never happen {}", fileId);
return false;
}
summary.setSummaryData(null); // remove json before saving to database
try (DBHandler dbHandler = GBApplication.acquireDB()) {
final DaoSession session = dbHandler.getDaoSession();
final Device device = DBHelper.getDevice(support.getDevice(), session);
final User user = DBHelper.getUser(session);
final BaseActivitySummary existingSummary = findOrCreateBaseActivitySummary(session, device, user, fileId);
existingSummary.setEndTime(summary.getEndTime());
existingSummary.setActivityKind(summary.getActivityKind());
existingSummary.setRawSummaryData(summary.getRawSummaryData());
existingSummary.setSummaryData(null); // remove json before saving to database
session.getBaseActivitySummaryDao().insertOrReplace(existingSummary);
} catch (final Exception e) {
GB.toast(support.getContext(), "Error saving activity summary", Toast.LENGTH_LONG, GB.ERROR, e);
return false;
}
return true;
}
@Override
public BaseActivitySummary parseBinaryData(final BaseActivitySummary summary) {
final ByteBuffer buf = ByteBuffer.wrap(summary.getRawSummaryData()).order(ByteOrder.LITTLE_ENDIAN);
final XiaomiActivityFileId fileId = XiaomiActivityFileId.from(buf);
XiaomiSimpleActivityParser parser = null;
switch (fileId.getSubtype()) {
case SPORTS_OUTDOOR_WALKING_V1:
summary.setActivityKind(ActivityKind.TYPE_WALKING);
parser = getOutdoorWalkingV1Parser(fileId);
break;
case SPORTS_OUTDOOR_RUNNING:
summary.setActivityKind(ActivityKind.TYPE_RUNNING);
// TODO
break;
case SPORTS_INDOOR_CYCLING:
parser = getIndoorCyclingParser(fileId);
break;
case SPORTS_FREESTYLE:
summary.setActivityKind(ActivityKind.TYPE_STRENGTH_TRAINING);
// TODO
break;
case SPORTS_ELLIPTICAL:
summary.setActivityKind(ActivityKind.TYPE_ELLIPTICAL_TRAINER);
// TODO
break;
case SPORTS_OUTDOOR_WALKING_V2:
parser = getOutdoorWalkingV2Parser(fileId);
break;
case SPORTS_OUTDOOR_CYCLING:
parser = getOutdoorCyclingParser(fileId);
break;
default:
LOG.warn("No workout summary parser for {}", fileId.getSubtypeCode());
break;
}
if (parser != null) {
parser.parse(summary, buf);
}
return summary;
}
@Nullable
private XiaomiSimpleActivityParser getIndoorCyclingParser(final XiaomiActivityFileId fileId) {
final int version = fileId.getVersion();
final int headerSize;
switch (version) {
case 8:
headerSize = 7;
break;
default:
LOG.warn("Unable to parse workout summary version {}", fileId.getVersion());
return null;
}
final XiaomiSimpleActivityParser.Builder builder = new XiaomiSimpleActivityParser.Builder();
builder.setHeaderSize(headerSize);
builder.addInt(TIME_START, UNIT_UNIX_EPOCH_SECONDS);
builder.addInt(TIME_END, UNIT_UNIX_EPOCH_SECONDS);
builder.addInt(ACTIVE_SECONDS, UNIT_SECONDS);
builder.addUnknown(4);
builder.addShort(CALORIES_BURNT, UNIT_KCAL);
builder.addUnknown(4);
builder.addByte(HR_AVG, UNIT_BPM);
builder.addByte(HR_MAX, UNIT_BPM);
builder.addByte(HR_MIN, UNIT_BPM);
builder.addFloat(TRAINING_EFFECT_AEROBIC, UNIT_NONE);
builder.addUnknown(1);
builder.addUnknown(1);
builder.addShort(RECOVERY_TIME, UNIT_HOURS);
builder.addInt(HR_ZONE_EXTREME, UNIT_SECONDS);
builder.addInt(HR_ZONE_ANAEROBIC, UNIT_SECONDS);
builder.addInt(HR_ZONE_AEROBIC, UNIT_SECONDS);
builder.addInt(HR_ZONE_FAT_BURN, UNIT_SECONDS);
builder.addInt(HR_ZONE_WARM_UP, UNIT_SECONDS);
builder.addUnknown(2);
builder.addUnknown(4);
builder.addFloat(TRAINING_EFFECT_ANAEROBIC, UNIT_NONE);
// FIXME identify field lengths to align with the header
builder.addUnknown(3);
builder.addInt("configuredTimeGoal", UNIT_SECONDS);
builder.addShort("configuredCaloriesGoal", UNIT_KCAL);
builder.addShort("maximumCaloriesGoal", UNIT_KCAL); // TODO: mhm?
builder.addUnknown(28);
builder.addShort(WORKOUT_LOAD, UNIT_NONE); // training load
builder.addUnknown(24);
builder.addByte("configuredSets", UNIT_NONE);
builder.addUnknown(13);
builder.addInt("startTime2", UNIT_SECONDS);
builder.addInt("endTime2", UNIT_SECONDS);
builder.addInt("goal", UNIT_NONE); // TODO match against goalType
builder.addInt("duration2", UNIT_SECONDS);
builder.addInt("intervalTime", UNIT_SECONDS);
builder.addUnknown(56);
builder.addInt("hrZoneExtreme2", UNIT_SECONDS);
builder.addInt("hrZoneAnaerobic2", UNIT_SECONDS);
builder.addInt("hrZoneAerobic2", UNIT_SECONDS);
builder.addInt("hrZoneFatBurn2", UNIT_SECONDS);
builder.addInt("hrZoneWarmUp2", UNIT_SECONDS);
builder.addUnknown(16);
builder.addShort("vitality_gain", UNIT_NONE);
builder.addShort("training_load2", UNIT_NONE);
builder.addShort("recovery_time2", UNIT_HOURS);
return builder.build();
}
@Nullable
private XiaomiSimpleActivityParser getOutdoorWalkingV1Parser(final XiaomiActivityFileId fileId) {
final int version = fileId.getVersion();
final int headerSize;
switch (version) {
case 4:
headerSize = 4;
break;
default:
LOG.warn("Unable to parse workout summary version {}", fileId.getVersion());
return null;
}
final XiaomiSimpleActivityParser.Builder builder = new XiaomiSimpleActivityParser.Builder();
builder.setHeaderSize(headerSize);
builder.addInt(TIME_START, UNIT_UNIX_EPOCH_SECONDS);
builder.addInt(TIME_END, UNIT_UNIX_EPOCH_SECONDS);
builder.addInt(ACTIVE_SECONDS, UNIT_SECONDS);
builder.addInt(DISTANCE_METERS, UNIT_METERS);
builder.addInt(CALORIES_BURNT, UNIT_KCAL);
builder.addInt(PACE_MAX, UNIT_SECONDS_PER_M);
builder.addInt(PACE_MIN, UNIT_SECONDS_PER_M);
builder.addUnknown(4);
builder.addInt(STEPS, UNIT_STEPS);
builder.addUnknown(2); // pace?
builder.addByte(HR_AVG, UNIT_BPM);
builder.addByte(HR_MAX, UNIT_BPM);
builder.addByte(HR_MIN, UNIT_BPM);
// FIXME identify field lengths to align with the header
builder.addUnknown(20);
builder.addFloat("recoveryValue", "recoveryValue");
builder.addUnknown(9);
builder.addByte(RECOVERY_TIME, UNIT_SECONDS);
builder.addUnknown(2);
builder.addInt(HR_ZONE_EXTREME, UNIT_SECONDS);
builder.addInt(HR_ZONE_ANAEROBIC, UNIT_SECONDS);
builder.addInt(HR_ZONE_AEROBIC, UNIT_SECONDS);
builder.addInt(HR_ZONE_FAT_BURN, UNIT_SECONDS);
builder.addInt(HR_ZONE_WARM_UP, UNIT_SECONDS);
builder.addInt("configured_time_goal", UNIT_SECONDS);
return builder.build();
}
@Nullable
private XiaomiSimpleActivityParser getOutdoorWalkingV2Parser(final XiaomiActivityFileId fileId) {
final int version = fileId.getVersion();
final int headerSize;
switch (version) {
case 4:
headerSize = 7;
break;
default:
LOG.warn("Unable to parse workout summary version {}", fileId.getVersion());
return null;
}
final XiaomiSimpleActivityParser.Builder builder = new XiaomiSimpleActivityParser.Builder();
builder.setHeaderSize(headerSize);
builder.addShort(XIAOMI_WORKOUT_TYPE, XIAOMI_WORKOUT_TYPE);
builder.addInt(TIME_START, UNIT_UNIX_EPOCH_SECONDS);
builder.addInt(TIME_END, UNIT_UNIX_EPOCH_SECONDS);
builder.addInt(ACTIVE_SECONDS, UNIT_SECONDS);
builder.addUnknown(4);
builder.addInt(DISTANCE_METERS, UNIT_METERS);
builder.addUnknown(2);
builder.addShort(CALORIES_BURNT, UNIT_KCAL);
builder.addUnknown(12);
builder.addInt(STEPS, UNIT_STEPS);
builder.addUnknown(2);
builder.addByte(HR_AVG, UNIT_BPM);
builder.addByte(HR_MAX, UNIT_BPM);
builder.addByte(HR_MIN, UNIT_BPM);
builder.addUnknown(20);
builder.addFloat("recoveryValue", "?");
builder.addUnknown(9);
builder.addByte(RECOVERY_TIME, UNIT_HOURS);
builder.addUnknown(2);
builder.addInt(HR_ZONE_EXTREME, UNIT_SECONDS);
builder.addInt(HR_ZONE_ANAEROBIC, UNIT_SECONDS);
builder.addInt(HR_ZONE_AEROBIC, UNIT_SECONDS);
builder.addInt(HR_ZONE_FAT_BURN, UNIT_SECONDS);
builder.addInt(HR_ZONE_WARM_UP, UNIT_SECONDS);
builder.addInt("configuredTimeGoal", UNIT_SECONDS);
builder.addShort("configuredCaloriesGoal", UNIT_KCAL);
builder.addInt("configuredDistanceGoal", UNIT_METERS);
builder.addUnknown(11);
builder.addShort(WORKOUT_LOAD, UNIT_NONE); // training load
builder.addUnknown(24);
builder.addByte("averageHR2", UNIT_BPM);
builder.addByte("maxHR2", UNIT_BPM);
builder.addByte("minHR2", UNIT_BPM);
builder.addUnknown(2);
builder.addByte(CADENCE_AVG, UNIT_SPM);
return builder.build();
}
@Nullable
private XiaomiSimpleActivityParser getOutdoorCyclingParser(final XiaomiActivityFileId fileId) {
final int version = fileId.getVersion();
final int headerSize;
switch (version) {
case 4:
headerSize = 6;
break;
default:
LOG.warn("Unable to parse workout summary version {}", fileId.getVersion());
return null;
}
final XiaomiSimpleActivityParser.Builder builder = new XiaomiSimpleActivityParser.Builder();
builder.setHeaderSize(headerSize);
builder.addShort(XIAOMI_WORKOUT_TYPE, XIAOMI_WORKOUT_TYPE);
builder.addInt(TIME_START, UNIT_UNIX_EPOCH_SECONDS);
builder.addInt(TIME_END, UNIT_UNIX_EPOCH_SECONDS);
builder.addInt(ACTIVE_SECONDS, UNIT_SECONDS);
builder.addUnknown(4);
builder.addInt(DISTANCE_METERS, UNIT_METERS);
builder.addUnknown(2);
builder.addShort(CALORIES_BURNT, UNIT_KCAL);
builder.addUnknown(4);
builder.addUnknown(4);
builder.addFloat(SPEED_MAX, UNIT_KMPH);
builder.addByte(HR_AVG, UNIT_BPM);
builder.addByte(HR_MAX, UNIT_BPM);
builder.addByte(HR_MIN, UNIT_BPM);
return builder.build();
}
}