/* Copyright (C) 2023-2024 Frank Ertl 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 . */ package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.conversation; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.List; import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; import nodomain.freeyourgadget.gadgetbridge.database.DBHelper; import nodomain.freeyourgadget.gadgetbridge.devices.withingssteelhr.WithingsSteelHRSampleProvider; import nodomain.freeyourgadget.gadgetbridge.entities.WithingsSteelHRActivitySample; import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind; import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.WithingsSteelHRDeviceSupport; import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.activity.ActivityEntry; import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.activity.WithingsActivityType; import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.WorkoutType; import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.ActivitySampleCalories; import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.ActivitySampleCalories2; import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.ActivitySampleDuration; import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.ActivitySampleMovement; import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.ActivitySampleSleep; import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.ActivitySampleTime; import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.ActivityHeartrate; import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.WithingsStructure; import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.WithingsStructureType; import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.Message; import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.WithingsMessageType; import nodomain.freeyourgadget.gadgetbridge.util.GB; public class ActivitySampleHandler extends AbstractResponseHandler { private static final Logger logger = LoggerFactory.getLogger(ActivitySampleHandler.class); private ActivityEntry activityEntry; private List activityEntries = new ArrayList<>(); private List heartrateEntries = new ArrayList<>(); public ActivitySampleHandler(WithingsSteelHRDeviceSupport support) { super(support); } @Override public void handleResponse(Message response) { List data = response.getDataStructures(); if (data != null) { handleActivityData(data, response.getType()); } } public void onSyncFinished() { mergeHeartrateSamplesIntoActivitySammples(); saveData(); } private void handleActivityData(List dataList, short activityType) { for (WithingsStructure data : dataList) { switch (data.getType()) { case WithingsStructureType.ACTIVITY_SAMPLE_TIME: handleTimestamp(data, activityType); break; case WithingsStructureType.ACTIVITY_SAMPLE_DURATION: handleDuration(data); break; case WithingsStructureType.ACTIVITY_SAMPLE_MOVEMENT: handleMovement(data); break; case WithingsStructureType.ACTIVITY_SAMPLE_CALORIES: handleCalories1(data); break; case WithingsStructureType.ACTIVITY_SAMPLE_CALORIES_2: handleCalories2(data); break; case WithingsStructureType.ACTIVITY_SAMPLE_SLEEP: handleSleep(data); break; case WithingsStructureType.ACTIVITY_SAMPLE_WALK: handleWalk(data); break; case WithingsStructureType.ACTIVITY_SAMPLE_RUN: handleRun(data); break; case WithingsStructureType.ACTIVITY_SAMPLE_SWIM: handleSwim(data); break; case WithingsStructureType.ACTIVITY_HR: handleHeartrate(data); break; case WithingsStructureType.WORKOUT_TYPE: handleWorkoutType(data); break; default: logger.info("Received yet unhandled activity data of type '{}' with data '{}'.", data.getType(), GB.hexdump(data.getRawData())); } } if (activityEntry != null) { addToList(activityEntry); } } private void handleTimestamp(WithingsStructure data, short activityType) { if (activityEntry != null) { addToList(activityEntry); } activityEntry = new ActivityEntry(); activityEntry.setIsHeartrate(activityType == WithingsMessageType.GET_HEARTRATE_SAMPLES); activityEntry.setTimestamp((int) (((ActivitySampleTime) data).getDate().getTime() / 1000)); } private void handleWorkoutType(WithingsStructure data) { WithingsActivityType activityType = WithingsActivityType.fromCode(((WorkoutType) data).getActivityType()); activityEntry.setRawKind(activityType.toActivityKind().getCode()); } private void handleDuration(WithingsStructure data) { activityEntry.setDuration(((ActivitySampleDuration) data).getDuration()); } private void handleHeartrate(WithingsStructure data) { activityEntry.setIsHeartrate(((ActivityHeartrate) data).getHeartrate()); } private void handleMovement(WithingsStructure data) { activityEntry.setRawKind(ActivityKind.UNKNOWN.getCode()); activityEntry.setSteps(((ActivitySampleMovement) data).getSteps()); activityEntry.setDistance(((ActivitySampleMovement) data).getDistance()); } private void handleWalk(WithingsStructure data) { activityEntry.setRawKind(ActivityKind.WALKING.getCode()); } private void handleRun(WithingsStructure data) { activityEntry.setRawKind(ActivityKind.RUNNING.getCode()); } private void handleSwim(WithingsStructure data) { activityEntry.setRawKind(ActivityKind.SWIMMING.getCode()); } private void handleSleep(WithingsStructure data) { ActivityKind sleepType; switch (((ActivitySampleSleep) data).getSleepType()) { case 0: sleepType = ActivityKind.LIGHT_SLEEP; activityEntry.setRawIntensity(10); break; case 2: sleepType = ActivityKind.DEEP_SLEEP; activityEntry.setRawIntensity(70); break; case 3: sleepType = ActivityKind.REM_SLEEP; activityEntry.setRawIntensity(80); break; default: sleepType = ActivityKind.LIGHT_SLEEP; activityEntry.setRawIntensity(50); } activityEntry.setRawKind(sleepType.getCode()); } private void handleCalories1(WithingsStructure data) { activityEntry.setRawIntensity(((ActivitySampleCalories) data).getMet()); activityEntry.setCalories(((ActivitySampleCalories) data).getCalories()); } private void handleCalories2(WithingsStructure data) { activityEntry.setRawIntensity(((ActivitySampleCalories2) data).getMet()); activityEntry.setCalories(((ActivitySampleCalories2) data).getCalories()); } private void addToList(ActivityEntry activityEntry) { if (activityEntry.isHeartrate()) { heartrateEntries.add(activityEntry); } else { activityEntries.add(activityEntry); } } private void saveData() { List activitySamples = new ArrayList<>(); for (ActivityEntry activityEntry : activityEntries) { convertToSampleAndAddToList(activitySamples, activityEntry); } for (ActivityEntry activityEntry : heartrateEntries) { convertToSampleAndAddToList(activitySamples, activityEntry); } writeToDB(activitySamples); } private void writeToDB(List activitySamples) { try (DBHandler dbHandler = GBApplication.acquireDB()) { Long userId = DBHelper.getUser(dbHandler.getDaoSession()).getId(); Long deviceId = DBHelper.getDevice(device, dbHandler.getDaoSession()).getId(); WithingsSteelHRSampleProvider provider = new WithingsSteelHRSampleProvider(device, dbHandler.getDaoSession()); for (WithingsSteelHRActivitySample sample : activitySamples) { sample.setDeviceId(deviceId); sample.setUserId(userId); } provider.addGBActivitySamples(activitySamples.toArray(new WithingsSteelHRActivitySample[0])); } catch (Exception ex) { logger.warn("Error saving activity data: " + ex.getLocalizedMessage()); } } private void mergeHeartrateSamplesIntoActivitySammples() { for (ActivityEntry heartrateEntry : heartrateEntries) { for (ActivityEntry activityEntry : activityEntries) { if (doActivitiesOverlap(heartrateEntry, activityEntry)) { updateHeartrateEntry(heartrateEntry, activityEntry); } } } } private boolean doActivitiesOverlap(ActivityEntry heartrateEntry, ActivityEntry activityEntry) { return activityEntry.getTimestamp() <= heartrateEntry.getTimestamp() && (activityEntry.getTimestamp() + activityEntry.getDuration()) >= heartrateEntry.getTimestamp(); } private void updateHeartrateEntry(ActivityEntry heartRateEntry, ActivityEntry activityEntry) { heartRateEntry.setRawKind(activityEntry.getRawKind()); heartRateEntry.setRawIntensity(activityEntry.getRawIntensity()); heartRateEntry.setDuration(activityEntry.getDuration() - (heartRateEntry.getTimestamp() - activityEntry.getTimestamp())); // If timestamps are exactly the same and only then, the heartrate entry would overwrite the activity entry in the DB, so we set more values. // If we would do so everytime, steps and so on would be multiplicated. if (heartRateEntry.getTimestamp() == activityEntry.getTimestamp()) { heartRateEntry.setSteps(activityEntry.getSteps()); heartRateEntry.setDistance(activityEntry.getDistance()); heartRateEntry.setCalories(activityEntry.getCalories()); } } private void convertToSampleAndAddToList(List activitySamples, ActivityEntry activityEntry) { WithingsSteelHRActivitySample sample = new WithingsSteelHRActivitySample(); sample.setTimestamp(activityEntry.getTimestamp()); sample.setDuration(activityEntry.getDuration()); sample.setHeartRate(activityEntry.getHeartrate()); sample.setSteps(activityEntry.getSteps()); sample.setRawKind(activityEntry.getRawKind()); sample.setCalories(activityEntry.getCalories()); sample.setDistance(activityEntry.getDistance()); sample.setRawIntensity(activityEntry.getRawIntensity()); activitySamples.add(sample); } }